// Dashboard Service Provider JavaScript // This file handles the dynamic charts and data visualization for the service provider dashboard class ServiceProviderDashboard { constructor(serviceProviderData) { this.serviceProviderData = serviceProviderData; // Initialize service requests from the main API data this.allRequests = serviceProviderData.client_requests || []; console.log('šŸ” CONSTRUCTOR DEBUG: Initialized with service requests from API:', this.allRequests.length); console.log('šŸ” CONSTRUCTOR DEBUG: Raw serviceProviderData:', serviceProviderData); console.log('šŸ” CONSTRUCTOR DEBUG: client_requests array:', serviceProviderData.client_requests); console.log('šŸ” CONSTRUCTOR DEBUG: services array:', serviceProviderData.services); console.log('šŸ” CONSTRUCTOR DEBUG: services count:', (serviceProviderData.services || []).length); console.log('šŸ” CONSTRUCTOR DEBUG: All request details:', this.allRequests.map(r => `${r.id}: ${r.status}`)); this.initializeCharts(); this.populateServicesList(); this.populateClientRequests(); } initializeCharts() { // Set global chart defaults Chart.defaults.font.family = "'Poppins', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; Chart.defaults.font.size = 12; Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; this.createRevenueChart(); this.createServiceDistributionChart(); this.createClientGrowthChart(); this.createServicePerformanceChart(); } createRevenueChart() { const ctx = document.getElementById('serviceRevenueChart'); if (!ctx) return; // Destroy existing chart if it exists to prevent "Canvas already in use" error if (window.revenueChart && typeof window.revenueChart.destroy === 'function') { window.revenueChart.destroy(); } const revenueHistory = this.serviceProviderData.revenue_history || []; // If no revenue history from API, create mock data based on monthly revenue let labels, amounts; if (revenueHistory.length > 0) { // Sort by date and get data const sortedHistory = revenueHistory.sort((a, b) => new Date(a.date) - new Date(b.date)); labels = sortedHistory.map(record => { const date = new Date(record.date); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }); amounts = sortedHistory.map(record => record.amount); } else { // Generate mock revenue data based on monthly revenue (prefer persistent *_usd field) const monthlyRevenue = (this.serviceProviderData.monthly_revenue_usd ?? this.serviceProviderData.monthly_revenue) || 0; const dailyAverage = Math.floor(monthlyRevenue / 30); labels = []; amounts = []; // Generate last 7 days of data for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); // Add some variation to daily amounts (±30%) const variation = (Math.random() - 0.5) * 0.6; const dailyAmount = Math.max(0, Math.floor(dailyAverage * (1 + variation))); amounts.push(dailyAmount); } } window.revenueChart = new Chart(ctx.getContext('2d'), { type: 'line', data: { labels: labels, datasets: [{ label: 'Daily Revenue', data: amounts, borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', borderWidth: 3, tension: 0.3, fill: true, pointRadius: 5, pointHoverRadius: 7, pointBackgroundColor: '#28a745', pointBorderColor: '#ffffff', pointBorderWidth: 2 }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return `Revenue: ${context.raw} TFP`; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Revenue (TFP)' } }, x: { title: { display: true, text: 'Date' } } } } }); } createServiceDistributionChart() { const ctx = document.getElementById('serviceDistributionChart'); if (!ctx) return; // Destroy existing chart if it exists if (window.serviceDistributionChart && typeof window.serviceDistributionChart.destroy === 'function') { window.serviceDistributionChart.destroy(); } const services = this.serviceProviderData.services || []; const serviceNames = services.map(service => service.name); const totalHours = services.map(service => service.total_hours); const colors = [ 'rgba(0, 123, 255, 0.8)', 'rgba(40, 167, 69, 0.8)', 'rgba(255, 193, 7, 0.8)', 'rgba(220, 53, 69, 0.8)', 'rgba(108, 117, 125, 0.8)', 'rgba(102, 16, 242, 0.8)' ]; window.serviceDistributionChart = new Chart(ctx.getContext('2d'), { type: 'doughnut', data: { labels: serviceNames, datasets: [{ data: totalHours, backgroundColor: colors.slice(0, serviceNames.length), borderColor: colors.slice(0, serviceNames.length).map(color => color.replace('0.8', '1')), borderWidth: 2 }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((context.raw / total) * 100).toFixed(1); return `${context.label}: ${context.raw} hours (${percentage}%)`; } } } } } }); } createClientGrowthChart() { const ctx = document.getElementById('clientGrowthChart'); if (!ctx) return; // Destroy existing chart if it exists if (window.clientGrowthChart && typeof window.clientGrowthChart.destroy === 'function') { window.clientGrowthChart.destroy(); } // Generate realistic client growth data based on current total clients const currentClients = this.serviceProviderData.total_clients || 0; const labels = []; const clientData = []; // Generate 6 months of growth data leading to current client count for (let i = 5; i >= 0; i--) { const date = new Date(); date.setMonth(date.getMonth() - i); labels.push(date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' })); // Calculate progressive growth to current client count const growthFactor = (6 - i) / 6; const monthlyClients = Math.floor(currentClients * growthFactor * (0.7 + Math.random() * 0.3)); clientData.push(Math.max(0, monthlyClients)); } // Ensure the last month shows current client count if (clientData.length > 0) { clientData[clientData.length - 1] = currentClients; } window.clientGrowthChart = new Chart(ctx.getContext('2d'), { type: 'bar', data: { labels: labels, datasets: [{ label: 'Total Clients', data: clientData, backgroundColor: 'rgba(255, 193, 7, 0.7)', borderColor: 'rgba(255, 193, 7, 1)', borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return `Clients: ${context.raw}`; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Number of Clients' } }, x: { title: { display: true, text: 'Time Period' } } } } }); } createServicePerformanceChart() { const ctx = document.getElementById('servicePerformanceChart'); if (!ctx) return; // Destroy existing chart if it exists if (window.servicePerformanceChart && typeof window.servicePerformanceChart.destroy === 'function') { window.servicePerformanceChart.destroy(); } const services = this.serviceProviderData.services || []; const serviceNames = services.map(service => service.name); const ratings = services.map(service => service.rating); const pricePerHour = services.map(service => (service.price_per_hour ?? service.price_per_hour_usd ?? service.hourly_rate ?? service.price_amount ?? 0)); window.servicePerformanceChart = new Chart(ctx.getContext('2d'), { type: 'scatter', data: { datasets: [{ label: 'Service Performance', data: serviceNames.map((name, index) => ({ x: ratings[index], y: pricePerHour[index], label: name })), backgroundColor: 'rgba(220, 53, 69, 0.7)', borderColor: 'rgba(220, 53, 69, 1)', borderWidth: 2, pointRadius: 8, pointHoverRadius: 10 }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { const point = context.raw; return `${point.label}: Rating ${point.x}, ${point.y} TFP/hour`; } } } }, scales: { x: { title: { display: true, text: 'Average Rating' }, min: 0, max: 5 }, y: { title: { display: true, text: 'Price per Hour (TFP)' }, beginAtZero: true } } } }); } async populateServicesList() { const tbody = document.getElementById('services-list-tbody'); if (!tbody) { console.error('šŸ” SERVICES DEBUG: services-list-tbody element not found!'); return; } // PHASE 2 FIX: Enhanced service loading with better error handling and consistency let allServices = this.serviceProviderData.services || []; console.log('šŸ” PHASE 2 FIX: Initial services from main API:', allServices.length); console.log('šŸ” PHASE 2 FIX: serviceProviderData:', this.serviceProviderData); // PHASE 2 FIX: Always try to get fresh services data to ensure consistency try { console.log('šŸ” PHASE 2 FIX: Fetching fresh services data...'); const timestamp = new Date().getTime(); const data = await window.apiJson(`/api/dashboard/products?_t=${timestamp}`); console.log('šŸ” PRODUCTS FIX: Unwrapped data:', data); // Support both new products-based and legacy services-based responses let freshServices = null; if (Array.isArray(data.products)) { freshServices = data.products; console.log('šŸ” PRODUCTS FIX: Found products array:', freshServices.length); } else if (Array.isArray(data.services)) { freshServices = data.services; console.log('šŸ” PRODUCTS FIX: Found services array:', freshServices.length); } else if (Array.isArray(data)) { freshServices = data; console.log('šŸ” PRODUCTS FIX: Found direct array:', freshServices.length); } if (freshServices) { allServices = freshServices; console.log('šŸ” PRODUCTS FIX: Loaded', allServices.length, 'fresh services/products from API'); // Update the main data to keep in sync this.serviceProviderData.services = allServices; } else { console.warn('šŸ” PRODUCTS FIX: Fresh API returned no services/products or failed:', data); } } catch (error) { console.error('šŸ” PHASE 2 FIX: Fresh API call failed:', error); // Fall back to existing data if API call fails } console.log('šŸ” PRODUCTS FIX: Final services array:', allServices); console.log('šŸ” PRODUCTS FIX: Final services count:', allServices.length); if (allServices.length > 0) { console.log('šŸ” PRODUCTS FIX: Populating table with', allServices.length, 'services/products'); allServices.forEach((item, index) => { console.log(`šŸ” PRODUCTS FIX: Item ${index}:`, item); }); tbody.innerHTML = allServices.map(item => { // Handle both legacy service format and new product format const service = this.convertProductToService(item); const statusClass = service.status === 'Active' ? 'success' : service.status === 'Paused' ? 'warning' : 'secondary'; const stars = this.generateStarRating(service.rating || 0); return `
${service.name || 'Unnamed Service'}
${service.category || 'General'}
${service.category || 'General'} ${service.status || 'Active'} ${service.hourly_rate || service.price_per_hour || service.price_per_hour_usd || service.price_amount || 0} TFC/hour ${service.clients || 0}
${service.rating || 0}
${stars}
${service.total_hours || 0} hours `; }).join(''); } else { console.log('šŸ” PHASE 2 FIX: No services found, showing empty message'); tbody.innerHTML = `

No services available

Create your first service to get started
`; } } populateClientRequests() { // Use service requests from the main API data ONLY (already loaded in constructor) console.log('šŸ” populateClientRequests called with allRequests:', this.allRequests.length); console.log('šŸ” API request statuses:', this.allRequests.map(r => `${r.id}: ${r.status}`)); // REMOVED: No fallback to mock data - only use API data for consistency // This ensures tab counts match the displayed content // Populate all tabs with API data only this.populateOpenRequests(); this.populateInProgressRequests(); this.populateCompletedRequests(); this.updateTabCounts(this.allRequests); } // PHASE 1 FIX: Enhanced method to refresh service requests data after status changes async refreshServiceRequestsData() { console.log('šŸ”„ PHASE 1 FIX: Refreshing service requests data from main API...'); try { // Add cache-busting parameter to ensure fresh data const timestamp = new Date().getTime(); const data = await window.apiJson(`/api/dashboard/service-provider-data?_t=${timestamp}`); console.log('šŸ”„ PHASE 1 FIX: Fresh data received:', data); // PHASE 1 FIX: Update both services and service requests from fresh API data this.serviceProviderData = data; this.allRequests = data.client_requests || []; console.log('šŸ”„ PHASE 1 FIX: Updated serviceProviderData with fresh data'); console.log('šŸ”„ PHASE 1 FIX: Updated allRequests array:', this.allRequests.length, 'requests'); console.log('šŸ”„ PHASE 1 FIX: Updated services array:', (data.services || []).length, 'services'); console.log('šŸ”„ PHASE 1 FIX: Request statuses after refresh:', this.allRequests.map(r => `${r.id}: ${r.status}`)); // PHASE 1 FIX: Refresh both services list and service requests await this.populateServicesList(); this.populateOpenRequests(); this.populateInProgressRequests(); this.populateCompletedRequests(); this.updateTabCounts(this.allRequests); console.log('šŸ”„ PHASE 1 FIX: Successfully refreshed all dashboard data'); } catch (error) { console.error('āŒ PHASE 1 FIX: Error refreshing dashboard data:', error); // PHASE 1 FIX: Show user-friendly error message const errorMessage = document.createElement('div'); errorMessage.className = 'alert alert-warning alert-dismissible fade show'; errorMessage.innerHTML = ` Refresh Failed: Unable to load latest data. Please refresh the page. `; // Insert error message at the top of the dashboard const dashboardContent = document.querySelector('.dashboard-content') || document.querySelector('.container-fluid'); if (dashboardContent) { dashboardContent.insertBefore(errorMessage, dashboardContent.firstChild); } } } populateOpenRequests() { const tbody = document.getElementById('client-requests-tbody'); if (!tbody) return; console.log('šŸ” populateOpenRequests - Total allRequests:', this.allRequests.length); console.log('šŸ” All request statuses:', this.allRequests.map(r => `${r.id}: ${r.status}`)); const openRequests = this.allRequests.filter(r => r.status === 'Pending' || r.status === 'Awaiting Details' || r.status === 'Quote Requested' || r.status === 'Open' ); console.log('šŸ” Filtered Open requests:', openRequests.length); console.log('šŸ” Open request details:', openRequests.map(r => `${r.id}: ${r.status}`)); if (openRequests.length > 0) { tbody.innerHTML = openRequests.map(request => { const statusClass = request.status === 'Quote Requested' ? 'info' : request.status === 'Awaiting Details' ? 'warning' : request.status === 'Pending' ? 'warning' : 'secondary'; const priorityClass = request.priority === 'High' ? 'danger' : request.priority === 'Medium' ? 'warning' : 'success'; return ` ${request.id} ${request.client_name} ${request.service_name} ${request.status} ${request.requested_date} ${request.estimated_hours} ${request.budget} TFP ${request.priority}
${request.status === 'Quote Requested' ? `` : ` ` }
`; }).join(''); } else { tbody.innerHTML = ` No open requests `; } } populateInProgressRequests() { const tbody = document.getElementById('progress-requests-tbody'); if (!tbody) return; console.log('šŸ” populateInProgressRequests - Total allRequests:', this.allRequests.length); console.log('šŸ” All request statuses:', this.allRequests.map(r => `${r.id}: ${r.status}`)); const inProgressRequests = this.allRequests.filter(r => r.status === 'In Progress' || r.status === 'Active' ); console.log('šŸ” Filtered In Progress requests:', inProgressRequests.length); console.log('šŸ” In Progress request details:', inProgressRequests.map(r => `${r.id}: ${r.status}`)); if (inProgressRequests.length > 0) { tbody.innerHTML = inProgressRequests.map(request => { const priorityClass = request.priority === 'High' ? 'danger' : request.priority === 'Medium' ? 'warning' : 'success'; // Calculate progress (default to 50% for in-progress requests, can be enhanced later) const progress = request.progress || 50; const progressBarClass = progress >= 75 ? 'bg-success' : progress >= 50 ? 'bg-info' : progress >= 25 ? 'bg-warning' : 'bg-danger'; return ` ${request.id} ${request.client_name} ${request.service_name} ${request.status} ${request.requested_date}
${progress}%
${request.budget} TFP ${request.priority}
`; }).join(''); } else { tbody.innerHTML = ` No requests in progress `; } } populateCompletedRequests() { const tbody = document.getElementById('completed-requests-tbody'); const paginationDiv = document.getElementById('completed-pagination'); if (!tbody || !paginationDiv) return; console.log('šŸ” populateCompletedRequests - Total allRequests:', this.allRequests.length); console.log('šŸ” All request statuses:', this.allRequests.map(r => `${r.id}: ${r.status}`)); // Filter completed requests from actual service requests data (API only) const completedRequests = this.allRequests.filter(r => r.status === 'Completed' || r.status === 'Delivered' ); console.log('šŸ” Filtered completed requests from API:', completedRequests.length); console.log('šŸ” Completed request details:', completedRequests.map(r => `${r.id}: ${r.status}`)); // ONLY use API data - no fallback to mock data to ensure consistency const requestsToShow = completedRequests; console.log('šŸ” Final requests to show in completed tab (API only):', requestsToShow.length); // Pagination settings const itemsPerPage = 5; const totalItems = requestsToShow.length; const totalPages = Math.ceil(totalItems / itemsPerPage); const currentPage = this._currentCompletedPage || 1; // Use delegated pagination state // Calculate items for current page const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const currentItems = requestsToShow.slice(startIndex, endIndex); // Populate table if (currentItems.length > 0) { tbody.innerHTML = currentItems.map(request => { // Handle both API format and mock format const completedDate = request.completed_date || request.requested_date; const totalHours = request.total_hours || request.estimated_hours || 'N/A'; const totalAmount = request.total_amount || request.budget || 'N/A'; const rating = request.rating || '4.5'; return ` ${request.id} ${request.client_name} ${request.service_name} ${request.status} ${completedDate} ${totalHours}${typeof totalHours === 'number' ? ' hours' : ''} ${totalAmount}${typeof totalAmount === 'number' ? ' TFP' : ''}
${rating}
${this.generateStarRating(parseFloat(rating))}
`; }).join(''); } else { tbody.innerHTML = ` No completed requests `; } // Populate pagination this.populatePagination(paginationDiv, currentPage, totalPages, totalItems, startIndex + 1, Math.min(endIndex, totalItems)); } populatePagination(container, currentPage, totalPages, totalItems, startItem, endItem) { if (totalPages <= 1) { container.innerHTML = ''; return; } let paginationHTML = `
Showing ${startItem}-${endItem} of ${totalItems} completed requests
`; container.innerHTML = paginationHTML; } updateTabCounts(requests) { console.log('šŸ” PHASE 2 FIX: Updating tab counts with requests:', requests.length); console.log('šŸ” PHASE 2 FIX: All request details:', requests.map(r => `${r.id}: ${r.status}`)); // PHASE 2 FIX: Enhanced status filtering with better validation const openRequests = requests.filter(r => { const status = r.status || ''; return status === 'Pending' || status === 'Awaiting Details' || status === 'Quote Requested' || status === 'Open'; }); const openCount = openRequests.length; console.log('šŸ” PHASE 2 FIX: Open requests:', openRequests.map(r => `${r.id}: ${r.status}`)); const inProgressRequests = requests.filter(r => { const status = r.status || ''; return status === 'In Progress' || status === 'Active'; }); const inProgressCount = inProgressRequests.length; console.log('šŸ” PHASE 2 FIX: In Progress requests:', inProgressRequests.map(r => `${r.id}: ${r.status}`)); const completedRequests = requests.filter(r => { const status = r.status || ''; return status === 'Completed' || status === 'Delivered'; }); const completedCount = completedRequests.length; console.log('šŸ” PHASE 2 FIX: Completed requests:', completedRequests.map(r => `${r.id}: ${r.status}`)); // PHASE 2 FIX: Validation check - ensure all requests are accounted for const totalCounted = openCount + inProgressCount + completedCount; if (totalCounted !== requests.length) { console.warn(`šŸ” PHASE 2 FIX: Tab count mismatch! Total requests: ${requests.length}, Counted: ${totalCounted}`); const uncategorized = requests.filter(r => { const status = r.status || ''; return !['Pending', 'Awaiting Details', 'Quote Requested', 'Open', 'In Progress', 'Active', 'Completed', 'Delivered'].includes(status); }); console.warn('šŸ” PHASE 2 FIX: Uncategorized requests:', uncategorized.map(r => `${r.id}: ${r.status}`)); } console.log(`šŸ” PHASE 2 FIX: Final counts - Open: ${openCount}, In Progress: ${inProgressCount}, Completed: ${completedCount}, Total: ${totalCounted}/${requests.length}`); // PHASE 2 FIX: Enhanced DOM updates with error handling const openCountElement = document.getElementById('open-count'); if (openCountElement) { openCountElement.textContent = openCount; openCountElement.setAttribute('data-count', openCount); // For debugging } else { console.warn('šŸ” PHASE 2 FIX: open-count element not found'); } const progressCountElement = document.getElementById('progress-count'); if (progressCountElement) { progressCountElement.textContent = inProgressCount; progressCountElement.setAttribute('data-count', inProgressCount); // For debugging } else { console.warn('šŸ” PHASE 2 FIX: progress-count element not found'); } const completedCountElement = document.getElementById('completed-count'); if (completedCountElement) { completedCountElement.textContent = completedCount; completedCountElement.setAttribute('data-count', completedCount); // For debugging } else { console.warn('šŸ” PHASE 2 FIX: completed-count element not found'); } // PHASE 2 FIX: Update tab badges for better visual feedback this.updateTabBadges(openCount, inProgressCount, completedCount); } // PHASE 2 FIX: New method to update tab badges updateTabBadges(openCount, inProgressCount, completedCount) { const openTab = document.querySelector('[data-bs-target="#open-requests"]'); const progressTab = document.querySelector('[data-bs-target="#in-progress-requests"]'); const completedTab = document.querySelector('[data-bs-target="#completed-requests"]'); if (openTab) { const badge = openTab.querySelector('.badge') || this.createTabBadge(); badge.textContent = openCount; badge.className = `badge ${openCount > 0 ? 'bg-warning' : 'bg-secondary'} ms-2`; if (!openTab.querySelector('.badge')) { openTab.appendChild(badge); } } if (progressTab) { const badge = progressTab.querySelector('.badge') || this.createTabBadge(); badge.textContent = inProgressCount; badge.className = `badge ${inProgressCount > 0 ? 'bg-primary' : 'bg-secondary'} ms-2`; if (!progressTab.querySelector('.badge')) { progressTab.appendChild(badge); } } if (completedTab) { const badge = completedTab.querySelector('.badge') || this.createTabBadge(); badge.textContent = completedCount; badge.className = `badge ${completedCount > 0 ? 'bg-success' : 'bg-secondary'} ms-2`; if (!completedTab.querySelector('.badge')) { completedTab.appendChild(badge); } } } // PHASE 2 FIX: Helper method to create tab badges createTabBadge() { const badge = document.createElement('span'); badge.className = 'badge bg-secondary ms-2'; return badge; } // Helper function to convert product format to service format for display convertProductToService(item) { // If it's already in service format, return as-is if (item.hourly_rate || item.price_per_hour || item.service_type) { return item; } // Convert product format to service format return { id: item.id, name: item.name, description: item.description, category: item.category_id || item.category || 'General', status: item.availability === 'Available' ? 'Active' : item.availability === 'OutOfStock' ? 'Paused' : 'Active', hourly_rate: item.base_price || 0, price_per_hour: item.base_price || 0, price_amount: item.base_price || 0, rating: item.metadata?.rating || 0, clients: item.metadata?.client_count || 0, total_hours: item.metadata?.total_hours || 0, provider_name: item.provider_name || 'Unknown Provider', created_at: item.created_at, updated_at: item.updated_at }; } generateStarRating(rating) { const fullStars = Math.floor(rating); const hasHalfStar = rating % 1 !== 0; const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); let stars = ''; for (let i = 0; i < fullStars; i++) { stars += ''; } if (hasHalfStar) { stars += ''; } for (let i = 0; i < emptyStars; i++) { stars += ''; } return stars; } } // Initialize dashboard when DOM is loaded - SINGLE INITIALIZATION document.addEventListener('DOMContentLoaded', function() { // Single-initialization guard for main Service Provider dashboard if (window.__serviceProviderInitialized) { return; } window.__serviceProviderInitialized = true; // Prevent multiple initializations if (window.dashboardInitialized) { console.log('šŸ”§ Dashboard already initialized, skipping...'); return; } // Initialize modal management for better UX initializeModalManagement(); // Prevent default form submissions inside modals to avoid page reload/flicker on Enter document.addEventListener('submit', function(e) { const form = e.target; // Create Service modal if (form.closest('#createServiceModal')) { e.preventDefault(); try { createNewService(); } catch (err) { console.warn('createNewService failed:', err); } return; } // Create SLA modal if (form.closest('#createSLAModal')) { e.preventDefault(); try { saveSLA(); } catch (err) { console.warn('saveSLA failed:', err); } return; } // Service Management modal (edit) if (form.closest('#serviceManagementModal')) { e.preventDefault(); if (typeof saveServiceChanges === 'function') { try { saveServiceChanges(); } catch (err) { console.warn('saveServiceChanges failed:', err); } } return; } }, true); // Try hydration first let hydrated = false; const hydrationEl = document.getElementById('sp-dashboard-hydration'); if (hydrationEl && typeof hydrationEl.textContent === 'string') { const raw = hydrationEl.textContent.trim(); if (raw.length === 0) { console.log('šŸ” INIT DEBUG: No hydration payload found; will fetch via API.'); } else { try { const data = JSON.parse(raw); console.log('šŸ” INIT DEBUG: Hydration data parsed:', data); const isUsableObject = data && typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length > 0; if (!isUsableObject) { console.log('šŸ” INIT DEBUG: Hydration is null/empty or not a usable object; will fetch via API.'); } else { // Destroy existing instance if it exists if (window.dashboardInstance) { console.log('šŸ”§ Destroying existing dashboard instance...'); window.dashboardInstance = null; } window.dashboardInstance = new ServiceProviderDashboard(data); window.dashboardInitialized = true; hydrated = true; // Load SLAs after dashboard initialization loadUserSLAs(); } } catch (e) { console.warn('āš ļø Hydration JSON parse failed, falling back to API fetch:', e); } } } if (hydrated) { return; } console.log('šŸ” INIT DEBUG: No hydration data, fetching service provider data...'); // Fetch service provider data from API (standardized via apiJson) window.apiJson('/api/dashboard/service-provider-data') .then(payload => { console.log('šŸ” INIT DEBUG: Unwrapped payload:', payload); console.log('šŸ” INIT DEBUG: Services in payload:', payload && payload.services); console.log('šŸ” INIT DEBUG: Services count in payload:', ((payload && payload.services) || []).length); // Destroy existing instance if it exists if (window.dashboardInstance) { console.log('šŸ”§ Destroying existing dashboard instance...'); window.dashboardInstance = null; } window.dashboardInstance = new ServiceProviderDashboard(payload || {}); window.dashboardInitialized = true; // Load SLAs after dashboard initialization loadUserSLAs(); }) .catch(error => { console.error('Error loading service provider dashboard data:', error); // Initialize with empty data as fallback if (!window.dashboardInstance) { window.dashboardInstance = new ServiceProviderDashboard({ active_services: 0, total_clients: 0, monthly_revenue_usd: 0, total_revenue_usd: 0, service_rating: 0.0, services: [], client_requests: [], revenue_history: [] }); window.dashboardInitialized = true; } // Load SLAs even in fallback case loadUserSLAs(); }); }); // Global Functions (accessible from HTML onclick attributes) // Modal Management function showModal(modalId) { const modal = new bootstrap.Modal(document.getElementById(modalId)); modal.show(); } function hideModal(modalId) { const modal = bootstrap.Modal.getInstance(document.getElementById(modalId)); if (modal) modal.hide(); } // Service Management Functions let currentServiceId = null; let currentServiceData = null; window.manageService = async function manageService(serviceId, serviceName) { try { showNotification(`Loading ${serviceName} management...`, 'info'); // Fetch comprehensive service data (via apiJson) const data = await window.apiJson(`/api/dashboard/services/${serviceId}/details`); const payload = (data && data.data) ? data.data : data; if (payload && payload.service) { currentServiceId = serviceId; currentServiceData = payload.service; // Show modal first showModal('serviceManagementModal'); // Wait for modal to be fully shown before populating const modal = document.getElementById('serviceManagementModal'); modal.addEventListener('shown.bs.modal', function modalShownHandler() { // Remove this event listener after first use modal.removeEventListener('shown.bs.modal', modalShownHandler); // Now populate the modal content populateServiceManagementModal(payload.service); showNotification('Service management loaded', 'success'); }); } else { throw new Error((data && data.message) || 'Failed to load service'); } } catch (error) { console.error('Error loading service management:', error); showNotification(`Failed to load service: ${error.message}`, 'error'); } } // Show the service management modal function showServiceManagementModal(serviceId, serviceName) { console.log('šŸ”§ SERVICE PROVIDER: Opening management modal for service:', serviceId, serviceName); // Fetch service details first (via apiJson) window.apiJson(`/api/dashboard/services/${serviceId}`) .then(result => { const data = (result && result.data) ? result.data : result; if (!data || data.success === false) { showNotification('Failed to load service details', 'error'); return; } // Show modal first showModal('serviceManagementModal'); // Wait for modal to be fully shown before populating const modal = document.getElementById('serviceManagementModal'); modal.addEventListener('shown.bs.modal', function modalShownHandler() { // Remove this specific listener to avoid memory leaks modal.removeEventListener('shown.bs.modal', modalShownHandler); // Now populate the modal content populateServiceManagementModal((data && data.service) || { id: serviceId, name: serviceName }); showNotification('Service management loaded', 'success'); }); }) .catch(error => { console.error('Error loading service details:', error); showNotification('Error loading service details', 'error'); }); } // Populate the service management modal with data function populateServiceManagementModal(service) { // Update modal title document.getElementById('serviceManagementModalLabel').textContent = `Manage Service: ${service.name}`; // Ensure the Details tab is active and visible before populating const detailsTab = document.getElementById('service-details'); const detailsTabButton = document.getElementById('service-details-tab'); if (detailsTab && detailsTabButton) { // Make sure the details tab is active detailsTabButton.classList.add('active'); detailsTab.classList.add('show', 'active'); // Remove active class from other tabs document.querySelectorAll('.nav-link').forEach(tab => { if (tab.id !== 'service-details-tab') { tab.classList.remove('active'); } }); document.querySelectorAll('.tab-pane').forEach(pane => { if (pane.id !== 'service-details') { pane.classList.remove('show', 'active'); } }); } // Use setTimeout to ensure DOM is ready before populating setTimeout(() => { // Populate details tab populateDetailsTab(service); // Populate settings tab (even if not visible) populateSettingsTab(service); }, 100); // Load analytics data loadServiceAnalytics(service.id); // Load client data loadServiceClients(service.id); } // Populate the details tab with service information function populateDetailsTab(service) { // Safely populate form fields with null checks const setFieldValue = (fieldId, value) => { const field = document.getElementById(fieldId); if (field) { field.value = value; } else { console.warn(`Field not found: ${fieldId}`); } }; setFieldValue('serviceManageName', service.name || ''); setFieldValue('serviceManageCategory', service.category || 'Infrastructure'); setFieldValue('serviceManageDescription', service.description || ''); setFieldValue('serviceManagePrice', service.price_per_hour || 0); setFieldValue('serviceManageResponseTime', service.availability?.response_time || '2 hours'); setFieldValue('serviceManageAvailability', service.availability?.weekly_hours || 20); setFieldValue('serviceManageExperience', service.experience_level || 'Expert'); // Handle skills array if (service.skills && Array.isArray(service.skills)) { setFieldValue('serviceManageSkills', service.skills.join(', ')); } else { setFieldValue('serviceManageSkills', ''); } } // Load and display service analytics async function loadServiceAnalytics(serviceId) { try { const data = await window.apiJson(`/api/dashboard/services/${serviceId}/analytics`); const analytics = (data && data.analytics) || (data && data.data && data.data.analytics) || data; if (analytics) { // Update analytics display document.getElementById('serviceAvgRating').textContent = analytics.performance.avg_rating.toFixed(1); document.getElementById('serviceOnTimeDelivery').textContent = `${analytics.performance.on_time_delivery.toFixed(0)}%`; document.getElementById('serviceCompletionRate').textContent = `${analytics.performance.completion_rate.toFixed(0)}%`; document.getElementById('serviceTotalRevenue').textContent = `${analytics.revenue.total} TFP`; document.getElementById('serviceMonthlyRevenue').textContent = `${analytics.revenue.monthly} TFP`; document.getElementById('serviceTotalProjects').textContent = analytics.project_metrics.total_projects; document.getElementById('serviceActiveClients').textContent = analytics.client_metrics.total_clients; // Create revenue chart createServiceRevenueAnalyticsChart(analytics.revenue.trend); } } catch (error) { console.error('Error loading service analytics:', error); showNotification('Failed to load analytics data', 'error'); } } // Create revenue analytics chart function createServiceRevenueAnalyticsChart(revenueData) { const ctx = document.getElementById('serviceRevenueAnalyticsChart'); if (!ctx) return; // Destroy existing chart if it exists if (window.serviceRevenueAnalyticsChart && typeof window.serviceRevenueAnalyticsChart.destroy === 'function') { window.serviceRevenueAnalyticsChart.destroy(); } // Generate labels for last 6 months const labels = []; for (let i = 5; i >= 0; i--) { const date = new Date(); date.setMonth(date.getMonth() - i); labels.push(date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' })); } window.serviceRevenueAnalyticsChart = new Chart(ctx.getContext('2d'), { type: 'line', data: { labels: labels, datasets: [{ label: 'Monthly Revenue', data: revenueData, borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', borderWidth: 3, tension: 0.3, fill: true, pointRadius: 5, pointHoverRadius: 7, pointBackgroundColor: '#28a745', pointBorderColor: '#ffffff', pointBorderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return `Revenue: ${context.raw} TFP`; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Revenue (TFP)' } }, x: { title: { display: true, text: 'Month' } } } } }); } // Load and display service clients async function loadServiceClients(serviceId) { try { const data = await window.apiJson(`/api/dashboard/services/${serviceId}/clients`); const clients = (data && data.clients) || (data && data.data && data.data.clients) || []; const summary = (data && data.summary) || (data && data.data && data.data.summary) || {}; if (clients && summary) { // Update client counts document.getElementById('clientsTotalCount').textContent = summary.total_clients; document.getElementById('clientsActiveCount').textContent = summary.active_clients; // Populate clients table const tbody = document.getElementById('serviceClientsTableBody'); const emptyDiv = document.getElementById('serviceClientsEmpty'); if (clients.length > 0) { tbody.style.display = ''; emptyDiv.style.display = 'none'; tbody.innerHTML = clients.map(client => { const stars = generateStarRating(client.avg_rating); const statusClass = client.status === 'Active' ? 'success' : 'secondary'; return `
${client.name}
${client.projects.length} projects
${client.email ? `
${client.email}
` : ''} ${client.phone ? `
${client.phone}
` : ''}
${client.projects.length} ${client.total_revenue} TFP
${client.avg_rating.toFixed(1)}
${stars}
${client.status} `; }).join(''); } else { tbody.style.display = 'none'; emptyDiv.style.display = 'block'; } } } catch (error) { console.error('Error loading service clients:', error); showNotification('Failed to load client data', 'error'); } } // Populate the settings tab function populateSettingsTab(service) { // Safely populate settings fields with null checks const setFieldValue = (fieldId, value) => { const field = document.getElementById(fieldId); if (field) { field.value = value; } else { console.warn(`Settings field not found: ${fieldId}`); } }; const setCheckboxValue = (fieldId, checked) => { const field = document.getElementById(fieldId); if (field) { field.checked = checked; } else { console.warn(`Settings checkbox not found: ${fieldId}`); } }; setFieldValue('serviceStatusSelect', service.status || 'Active'); setFieldValue('serviceSLASelect', service.sla_id || ''); setFieldValue('serviceMaxConcurrentProjects', service.max_concurrent_projects || 3); // Set checkbox values if they exist in service data setCheckboxValue('serviceAutoAccept', service.auto_accept || false); setCheckboxValue('serviceInstantQuotes', service.instant_quotes || false); setCheckboxValue('serviceEmailNotifications', service.email_notifications !== false); // Default to true // Load user SLAs for the SLA dropdown loadUserSLAsForService(); } // Load user SLAs for the service settings async function loadUserSLAsForService() { try { const data = await window.apiJson('/api/dashboard/slas'); if (data) { const slaSelect = document.getElementById('serviceSLASelect'); slaSelect.innerHTML = ''; const slas = Array.isArray(data) ? data : (data.slas || (data.data && data.data.slas) || []); slas.forEach(sla => { const option = document.createElement('option'); option.value = sla.id; option.textContent = sla.name; slaSelect.appendChild(option); }); } } catch (error) { console.error('Error loading SLAs:', error); } } // Update service status window.updateServiceStatus = async function updateServiceStatus() { if (!currentServiceId) return; const newStatus = document.getElementById('serviceStatusSelect').value; try { showNotification('Updating service status...', 'info'); await window.apiJson(`/api/dashboard/services/${currentServiceId}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: newStatus }) }); showNotification('Service status updated successfully', 'success'); // Update current service data if (currentServiceData) { currentServiceData.status = newStatus; } } catch (error) { console.error('Error updating service status:', error); showNotification(`Failed to update status: ${error.message}`, 'error'); } } // Save all service changes window.saveServiceChanges = async function saveServiceChanges() { console.log('saveServiceChanges called, currentServiceId:', currentServiceId); if (!currentServiceId) { console.error('No currentServiceId set'); showNotification('Error: No service selected', 'error'); return; } try { showNotification('Saving changes...', 'info'); // Ensure all tabs are accessible by temporarily activating them const allTabs = ['service-details', 'service-analytics', 'service-clients', 'service-settings']; const originalActiveTab = document.querySelector('.nav-link.active').getAttribute('data-bs-target'); // Briefly activate each tab to ensure DOM elements are rendered for (const tabId of allTabs) { const tabElement = document.getElementById(tabId); if (tabElement) { tabElement.classList.add('show', 'active'); } } // Small delay to ensure DOM is ready await new Promise(resolve => setTimeout(resolve, 50)); // Collect form data from all tabs const nameField = document.getElementById('serviceManageName'); const categoryField = document.getElementById('serviceManageCategory'); const descriptionField = document.getElementById('serviceManageDescription'); const priceField = document.getElementById('serviceManagePrice'); const statusField = document.getElementById('serviceStatusSelect'); const responseTimeField = document.getElementById('serviceManageResponseTime'); const availabilityField = document.getElementById('serviceManageAvailability'); const experienceField = document.getElementById('serviceManageExperience'); const skillsField = document.getElementById('serviceManageSkills'); // Collect additional settings fields const slaField = document.getElementById('serviceSLASelect'); const maxProjectsField = document.getElementById('serviceMaxConcurrentProjects'); const autoAcceptField = document.getElementById('serviceAutoAccept'); const instantQuotesField = document.getElementById('serviceInstantQuotes'); const emailNotificationsField = document.getElementById('serviceEmailNotifications'); // Debug logging to identify missing fields (only check required fields) const requiredFieldChecks = { 'serviceManageName': nameField, 'serviceManageCategory': categoryField, 'serviceManageDescription': descriptionField, 'serviceManagePrice': priceField, 'serviceStatusSelect': statusField }; // Optional fields (log warnings but don't fail) const optionalFieldChecks = { 'serviceManageResponseTime': responseTimeField, 'serviceManageAvailability': availabilityField, 'serviceManageExperience': experienceField, 'serviceManageSkills': skillsField, 'serviceSLASelect': slaField, 'serviceMaxConcurrentProjects': maxProjectsField, 'serviceAutoAccept': autoAcceptField, 'serviceInstantQuotes': instantQuotesField, 'serviceEmailNotifications': emailNotificationsField }; const missingRequiredFields = []; for (const [fieldName, field] of Object.entries(requiredFieldChecks)) { if (!field) { missingRequiredFields.push(fieldName); console.error(`Required field not found: ${fieldName}`); } } if (missingRequiredFields.length > 0) { console.error('Missing required form fields:', missingRequiredFields); showNotification(`Error: Missing required fields: ${missingRequiredFields.join(', ')}`, 'error'); return; } // Log warnings for missing optional fields for (const [fieldName, field] of Object.entries(optionalFieldChecks)) { if (!field) { console.warn(`Optional field not found: ${fieldName}`); } } // Collect skills array const skillsArray = skillsField.value ? skillsField.value.split(',').map(s => s.trim()).filter(s => s) : []; const formData = { id: currentServiceId, name: nameField.value || '', category: categoryField.value || 'Infrastructure', description: descriptionField.value || '', price_per_hour: parseInt(priceField.value) || 0, status: statusField.value || 'Active', skills: skillsArray, availability: { response_time: responseTimeField.value || '2 hours', weekly_hours: parseInt(availabilityField.value) || 20 }, experience_level: experienceField.value || 'Expert', // Settings fields sla_id: slaField ? slaField.value : '', max_concurrent_projects: maxProjectsField ? parseInt(maxProjectsField.value) || 3 : 3, auto_accept: autoAcceptField ? autoAcceptField.checked : false, instant_quotes: instantQuotesField ? instantQuotesField.checked : false, email_notifications: emailNotificationsField ? emailNotificationsField.checked : true, // Keep existing values for fields not in the form clients: currentServiceData?.clients || 0, rating: currentServiceData?.rating || 0, total_hours: currentServiceData?.total_hours || 0 }; // Restore original tab state allTabs.forEach(tabId => { const tabElement = document.getElementById(tabId); if (tabElement && `#${tabId}` !== originalActiveTab) { tabElement.classList.remove('show', 'active'); } }); console.log('Form data collected:', formData); await window.apiJson(`/api/dashboard/services/${currentServiceId}`, { method: 'PUT', body: formData }); showNotification('Service updated successfully', 'success'); // Update the current service data with the new values currentServiceData = { ...currentServiceData, ...formData }; // Close the modal const modal = bootstrap.Modal.getInstance(document.getElementById('serviceManagementModal')); if (modal) { modal.hide(); } // Refresh dashboard data if (window.dashboardInstance) { await window.dashboardInstance.populateServicesList(); window.dashboardInstance.createServiceDistributionChart(); window.dashboardInstance.createServicePerformanceChart(); } } catch (error) { console.error('Error saving service changes:', error); showNotification(`Failed to save: ${error.message}`, 'error'); } } // Remove the old editService function since we're consolidating into manageService // function editService() - REMOVED // Client Request Management Functions function viewRequest(requestId, clientName) { showNotification(`Loading request details from ${clientName}...`, 'info'); // Fetch request details from API window.apiJson(`/api/dashboard/service-requests/${requestId}/details`) .then(data => { const request = (data && data.request) || (data && data.data && data.data.request) || data; if (request) { populateRequestDetailsModal(request); const modal = new bootstrap.Modal(document.getElementById('requestDetailsModal')); modal.show(); showNotification(`Request ${requestId} details loaded`, 'success'); } else { throw new Error((data && data.message) || 'Failed to load request details'); } }) .catch(error => { console.error('Error loading request details:', error); showNotification(`Failed to load request details: ${error.message}`, 'error'); }); } // Populate request details modal with data function populateRequestDetailsModal(request) { document.getElementById('detailRequestId').textContent = request.id; document.getElementById('detailClientName').textContent = request.client_name; document.getElementById('detailServiceName').textContent = request.service_name; // Status badge with appropriate color const statusBadge = document.getElementById('detailStatus'); statusBadge.textContent = request.status; statusBadge.className = `badge ${getStatusBadgeClass(request.status)}`; // Priority badge with appropriate color const priorityBadge = document.getElementById('detailPriority'); priorityBadge.textContent = request.priority; priorityBadge.className = `badge ${getPriorityBadgeClass(request.priority)}`; document.getElementById('detailRequestedDate').textContent = request.requested_date; document.getElementById('detailEstimatedHours').textContent = `${request.estimated_hours} hours`; document.getElementById('detailBudget').textContent = `${request.budget} TFP`; // Progress bar const progress = request.progress || 0; const progressBar = document.getElementById('detailProgressBar'); progressBar.style.width = `${progress}%`; progressBar.textContent = `${progress}%`; progressBar.className = `progress-bar ${getProgressBarClass(progress)}`; // Description and notes document.getElementById('detailDescription').textContent = request.description || 'No description provided.'; document.getElementById('detailNotes').textContent = request.notes || 'No notes available.'; // Store request ID for progress updates document.getElementById('progressUpdateModal').setAttribute('data-request-id', request.id); // Show/hide update progress button based on status const updateBtn = document.getElementById('detailUpdateProgressBtn'); updateBtn.style.display = (request.status === 'In Progress') ? 'block' : 'none'; } // Helper functions for styling function getStatusBadgeClass(status) { switch(status) { case 'Open': return 'bg-warning text-dark'; case 'In Progress': return 'bg-primary'; case 'Completed': return 'bg-success'; case 'Declined': return 'bg-danger'; default: return 'bg-secondary'; } } function getPriorityBadgeClass(priority) { switch(priority) { case 'High': return 'bg-danger'; case 'Medium': return 'bg-warning text-dark'; case 'Low': return 'bg-info'; default: return 'bg-secondary'; } } function getProgressBarClass(progress) { if (progress >= 100) return 'bg-success'; if (progress >= 75) return 'bg-info'; if (progress >= 50) return 'bg-warning'; return 'bg-primary'; } // Open progress update modal function openProgressUpdateModal() { const requestId = document.getElementById('progressUpdateModal').getAttribute('data-request-id'); if (requestId) { // First, properly hide the details modal to prevent overlap const detailsModal = bootstrap.Modal.getInstance(document.getElementById('requestDetailsModal')); if (detailsModal) { detailsModal.hide(); } // Wait for the details modal to fully close before opening progress modal setTimeout(() => { // Reset form document.getElementById('progressUpdateForm').reset(); document.getElementById('progressSlider').value = 50; document.getElementById('progressValue').textContent = '50'; // Store reference to return to details modal if needed document.getElementById('progressUpdateModal').setAttribute('data-return-to-details', 'true'); const progressModal = new bootstrap.Modal(document.getElementById('progressUpdateModal')); progressModal.show(); }, 300); // Wait for modal transition to complete } } // Update progress value display function updateProgressValue(value) { document.getElementById('progressValue').textContent = value; } // Save progress update async function saveProgressUpdate() { const requestId = document.getElementById('progressUpdateModal').getAttribute('data-request-id'); const progress = parseInt(document.getElementById('progressSlider').value); const priority = document.getElementById('progressPriority').value; const hours = parseFloat(document.getElementById('progressHours').value) || 0; const notes = document.getElementById('progressNotes').value; const notifyClient = document.getElementById('notifyClient').checked; if (!requestId) { showNotification('Error: No request ID found', 'error'); return; } showNotification('Updating progress...', 'info'); // Prepare update data const updateData = { progress: progress, priority: priority, hours_worked: hours, notes: notes, notify_client: notifyClient, status: progress === 100 ? 'Completed' : 'In Progress' }; try { await window.apiJson(`/api/dashboard/service-requests/${requestId}/progress`, { method: 'PUT', body: updateData }); showNotification(`Progress updated to ${progress}%`, 'success'); const progressModal = bootstrap.Modal.getInstance(document.getElementById('progressUpdateModal')); if (progressModal) { progressModal.hide(); } document.getElementById('progressUpdateModal').removeAttribute('data-return-to-details'); if (window.dashboardInstance) { setTimeout(() => { window.dashboardInstance.refreshServiceRequestsData(); }, 500); } } catch (error) { console.error('Error updating progress:', error); showNotification(`Failed to update progress: ${error.message}`, 'error'); if (document.getElementById('progressUpdateModal').getAttribute('data-return-to-details') === 'true') { backToRequestDetails(); } } } // Function to go back to request details from progress modal function backToRequestDetails() { const requestId = document.getElementById('progressUpdateModal').getAttribute('data-request-id'); if (requestId) { // Close progress modal const progressModal = bootstrap.Modal.getInstance(document.getElementById('progressUpdateModal')); if (progressModal) { progressModal.hide(); } // Wait for modal to close, then reopen details modal setTimeout(() => { // Re-fetch and show the details modal (via apiJson) window.apiJson(`/api/dashboard/service-requests/${requestId}/details`) .then(data => { const request = (data && data.request) || (data && data.data && data.data.request) || data; if (request) { populateRequestDetailsModal(request); const detailsModal = new bootstrap.Modal(document.getElementById('requestDetailsModal')); detailsModal.show(); } else { showNotification('Failed to reload request details', 'error'); } }) .catch(error => { console.error('Error reloading request details:', error); showNotification('Failed to reload request details', 'error'); }); }, 300); } } async function acceptRequest(requestId, clientName, serviceName) { if (confirm(`Accept service request from ${clientName} for ${serviceName}?`)) { showNotification(`Accepting request from ${clientName}...`, 'info'); try { await window.apiJson(`/api/dashboard/service-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: 'In Progress' }) }); showNotification(`Request ${requestId} accepted successfully`, 'success'); if (window.dashboardInstance) { console.log('šŸ”„ Immediately refreshing dashboard data after accept...'); window.dashboardInstance.refreshServiceRequestsData(); } console.log(`Accepted request: ${requestId} from ${clientName} for ${serviceName}`); } catch (error) { console.error('Error accepting request:', error); showNotification(`Failed to accept request: ${error.message}`, 'error'); } } } function declineRequest(requestId, clientName) { // Set up the modal with client name and request ID document.getElementById('declineClientName').textContent = clientName; // Store request data for the confirmation button const confirmBtn = document.getElementById('confirmDeclineBtn'); confirmBtn.setAttribute('data-request-id', requestId); confirmBtn.setAttribute('data-client-name', clientName); // Show the modal const modal = new bootstrap.Modal(document.getElementById('declineConfirmModal')); modal.show(); } // Delegated confirmation handled via data-action="request.decline.confirm" async function sendQuote(requestId, clientName) { showNotification(`Preparing quote for ${clientName}...`, 'info'); try { await window.apiJson(`/api/dashboard/service-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: 'Quote Sent' }) }); showNotification(`Quote sent to ${clientName} successfully`, 'success'); if (window.dashboardInstance) { console.log('šŸ”„ Immediately refreshing dashboard data after quote sent...'); window.dashboardInstance.refreshServiceRequestsData(); } console.log(`Quote sent for request: ${requestId} to ${clientName}`); } catch (error) { console.error('Error sending quote:', error); showNotification(`Failed to send quote: ${error.message}`, 'error'); } } function completeRequest(requestId, clientName) { // Set the client name in the modal document.getElementById('completeClientName').textContent = clientName; // Set attributes for delegated confirmation handler const confirmBtn = document.getElementById('confirmCompleteBtn'); confirmBtn.setAttribute('data-request-id', requestId); confirmBtn.setAttribute('data-client-name', clientName); // Show the modal const modal = new bootstrap.Modal(document.getElementById('completeConfirmModal')); modal.show(); } async function updateProgress(requestId, clientName) { showNotification(`Opening progress update for ${clientName}...`, 'info'); // Set the request ID for the modal document.getElementById('progressUpdateModal').setAttribute('data-request-id', requestId); // Reset form to defaults document.getElementById('progressUpdateForm').reset(); document.getElementById('progressSlider').value = 50; document.getElementById('progressValue').textContent = '50'; // Open the progress update modal const modal = new bootstrap.Modal(document.getElementById('progressUpdateModal')); modal.show(); showNotification('Progress update form opened', 'success'); console.log(`Updating progress for request: ${requestId} from ${clientName}`); } // View completed request details async function viewCompletedRequest(requestId, serviceType, client) { showNotification(`Loading completed request details...`, 'info'); try { const data = await window.apiJson(`/api/dashboard/service-requests/${requestId}/completed-details`); const request = (data && data.request) ? data.request : data; populateCompletedRequestModal(request); const modal = new bootstrap.Modal(document.getElementById('completedRequestModal')); modal.show(); showNotification(`Completed request details loaded`, 'success'); } catch (error) { console.error('Error loading completed request details:', error); showNotification(`Failed to load request details: ${error.message}`, 'error'); } } // Populate completed request modal with data function populateCompletedRequestModal(request) { document.getElementById('completedRequestId').textContent = request.id; document.getElementById('completedClientName').textContent = request.client_name; document.getElementById('completedServiceType').textContent = request.service_name; document.getElementById('completedDate').textContent = request.completed_date || 'N/A'; document.getElementById('completedHoursLogged').textContent = `${request.hours_logged || 0} hours`; document.getElementById('completedRevenue').textContent = `${request.revenue || 0} TFP`; document.getElementById('completedRating').textContent = request.rating ? `${request.rating}/5 ⭐` : 'Not rated'; document.getElementById('completedOnTime').textContent = request.on_time ? 'Yes āœ…' : 'No āŒ'; document.getElementById('completedSummary').textContent = request.summary || 'No summary available.'; document.getElementById('completedFeedback').textContent = request.client_feedback || 'No feedback provided.'; // Store request ID for invoice generation document.getElementById('completedRequestModal').setAttribute('data-request-id', request.id); } // Generate invoice for completed request function generateInvoice() { const requestId = document.getElementById('completedRequestModal').getAttribute('data-request-id'); if (!requestId) { showNotification('Error: No request ID found', 'error'); return; } showNotification('Generating invoice...', 'info'); // Make API call to generate invoice fetch(`/api/dashboard/service-requests/${requestId}/invoice`, { method: 'GET', headers: { 'Accept': 'text/plain', } }) .then(response => { if (response.ok) { return response.blob(); } else { throw new Error('Failed to generate invoice'); } }) .then(blob => { // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `invoice-${requestId}.txt`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); showNotification('Invoice downloaded successfully', 'success'); }) .catch(error => { console.error('Error generating invoice:', error); showNotification(`Failed to generate invoice: ${error.message}`, 'error'); }); } // Download project report function downloadReport() { const requestId = document.getElementById('completedRequestModal').getAttribute('data-request-id'); if (!requestId) { showNotification('Error: No request ID found', 'error'); return; } showNotification('Generating project report...', 'info'); // For now, create a simple text report (can be enhanced to PDF later) const reportContent = generateReportContent(requestId); const blob = new Blob([reportContent], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `project-report-${requestId}.txt`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); showNotification('Project report downloaded', 'success'); } // Generate report content function generateReportContent(requestId) { const modal = document.getElementById('completedRequestModal'); const clientName = document.getElementById('completedClientName').textContent; const serviceType = document.getElementById('completedServiceType').textContent; const completedDate = document.getElementById('completedDate').textContent; const hoursLogged = document.getElementById('completedHoursLogged').textContent; const revenue = document.getElementById('completedRevenue').textContent; const rating = document.getElementById('completedRating').textContent; const summary = document.getElementById('completedSummary').textContent; const feedback = document.getElementById('completedFeedback').textContent; return `PROJECT COMPLETION REPORT ======================== Request ID: ${requestId} Client: ${clientName} Service Type: ${serviceType} Completion Date: ${completedDate} Hours Logged: ${hoursLogged} Revenue Generated: ${revenue} Client Rating: ${rating} PROJECT SUMMARY =============== ${summary} CLIENT FEEDBACK =============== ${feedback} Generated on: ${new Date().toLocaleString()} `; } // Handle invoice download (called from table) function downloadInvoice(requestId) { showNotification('Generating invoice...', 'info'); // Make API call to generate invoice directly fetch(`/api/dashboard/service-requests/${requestId}/invoice`, { method: 'GET', headers: { 'Accept': 'application/pdf', } }) .then(response => { if (response.ok) { return response.blob(); } else { throw new Error('Failed to generate invoice'); } }) .then(blob => { // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `invoice-${requestId}.pdf`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); showNotification('Invoice downloaded successfully', 'success'); }) .catch(error => { console.error('Error generating invoice:', error); showNotification(`Failed to generate invoice: ${error.message}`, 'error'); }); } // Handle service report viewing (called from table) function viewServiceReport(requestId) { console.log('Opening service report for request:', requestId); // Open the service report in a new tab/window const reportUrl = `/api/dashboard/service-requests/${requestId}/report`; window.open(reportUrl, '_blank'); showNotification('Opening service report...', 'info'); } // Handle service invoice viewing (called from table) function viewServiceInvoice(requestId) { console.log('Opening service invoice for request:', requestId); // Open the service invoice in a new tab/window const invoiceUrl = `/api/dashboard/service-requests/${requestId}/invoice`; window.open(invoiceUrl, '_blank'); showNotification('Opening service invoice...', 'info'); } // Tab switching function - SINGLE IMPLEMENTATION function switchServiceRequestTab(tabName) { console.log(`Switching to ${tabName} tab`); // Refresh the appropriate tab content with current data if (window.dashboardInstance) { console.log(`Switching to ${tabName} tab, refreshing content with ${window.dashboardInstance.allRequests.length} requests`); switch(tabName) { case 'open': window.dashboardInstance.populateOpenRequests(); break; case 'progress': window.dashboardInstance.populateInProgressRequests(); break; case 'completed': window.dashboardInstance.populateCompletedRequests(); break; } // Update tab counts to ensure consistency window.dashboardInstance.updateTabCounts(window.dashboardInstance.allRequests); } } // Notification function - SINGLE IMPLEMENTATION function showNotification(message, type = 'info') { // Remove existing notifications const existingNotifications = document.querySelectorAll('.dashboard-notification'); existingNotifications.forEach(notification => notification.remove()); // Create notification element const notification = document.createElement('div'); notification.className = `alert alert-${getBootstrapAlertClass(type)} alert-dismissible fade show dashboard-notification`; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px; max-width: 500px; `; notification.innerHTML = ` ${message} `; // Add to page document.body.appendChild(notification); // Auto-remove after 5 seconds setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 5000); } function getBootstrapAlertClass(type) { const typeMap = { 'success': 'success', 'error': 'danger', 'warning': 'warning', 'info': 'info' }; return typeMap[type] || 'info'; } // Modal management improvements function initializeModalManagement() { // Add event listeners for better modal management const progressModal = document.getElementById('progressUpdateModal'); const detailsModal = document.getElementById('requestDetailsModal'); if (progressModal) { // Clean up when progress modal is hidden progressModal.addEventListener('hidden.bs.modal', function () { // Clear any stored data this.removeAttribute('data-request-id'); this.removeAttribute('data-return-to-details'); // Reset form const form = document.getElementById('progressUpdateForm'); if (form) { form.reset(); document.getElementById('progressSlider').value = 50; document.getElementById('progressValue').textContent = '50'; } }); } if (detailsModal) { // Clean up when details modal is hidden detailsModal.addEventListener('hidden.bs.modal', function () { // Clear any stored data this.removeAttribute('data-request-id'); }); } } // SLA Management Functions async function loadUserSLAs() { console.log('šŸ”§ DEBUG: loadUserSLAs called'); try { const data = await window.apiJson('/api/dashboard/slas'); console.log('šŸ”§ DEBUG: API raw SLAs data:', data); // Support both legacy {success, slas} and wrapped {success, data: {slas}} or even array payloads let slas = []; if (Array.isArray(data)) { slas = data; } else if (Array.isArray(data.slas)) { slas = data.slas; } else if (data && Array.isArray(data.data)) { slas = data.data; } else if (data && data.data && Array.isArray(data.data.slas)) { slas = data.data.slas; } if (slas && slas.length >= 0) { console.log('šŸ”§ DEBUG: Calling refreshSLADisplay with', slas.length, 'SLAs'); refreshSLADisplay(slas); } else { console.log('šŸ”§ DEBUG: API call failed or no SLAs returned'); } } catch (error) { console.error('šŸ”§ DEBUG: Error loading SLAs:', error); } } function refreshSLADisplay(slas) { console.log('šŸ”§ DEBUG: refreshSLADisplay called with', slas.length, 'SLAs'); // Prefer stable container ID if present, fallback to header scan let slaListGroup = document.getElementById('slaListGroup'); if (!slaListGroup) { const slaCards = document.querySelectorAll('.card-header h5'); console.log('šŸ”§ DEBUG: Found', slaCards.length, 'card headers'); for (let header of slaCards) { console.log('šŸ”§ DEBUG: Checking header:', header.textContent); if (header.textContent.includes('Service Level Agreements')) { slaListGroup = header.closest('.card').querySelector('.list-group'); console.log('šŸ”§ DEBUG: Found SLA list-group container via fallback:', !!slaListGroup); break; } } } if (!slaListGroup) { console.log('šŸ”§ DEBUG: SLA list-group container not found!'); return; } console.log('šŸ”§ DEBUG: Current container innerHTML length:', slaListGroup.innerHTML.length); // Simple innerHTML replacement (like wallet does) if (slas.length === 0) { slaListGroup.innerHTML = `

No SLAs created yet

Create your first SLA to get started
`; } else { slaListGroup.innerHTML = slas.map(sla => { const statusClass = sla.status === 'Active' ? 'primary' : sla.status === 'Draft' ? 'secondary' : 'warning'; return `
${sla.name}
Response: ${sla.response_time_hours}h | Resolution: ${sla.resolution_time_hours}h | Availability: ${sla.availability_percentage}%
${sla.status}
`; }).join(''); } console.log('šŸ”§ DEBUG: New innerHTML length:', slaListGroup.innerHTML.length); console.log('šŸ”§ DEBUG: SLA display updated successfully'); } // Centralized delegated event handling for CSP compliance document.addEventListener('click', function(e) { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; switch (action) { case 'request.view': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; if (requestId && clientName) viewRequest(requestId, clientName); break; } case 'request.update': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; if (requestId && clientName) updateProgress(requestId, clientName); break; } case 'request.accept': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; const serviceName = actionEl.dataset.serviceName; if (requestId && clientName && serviceName) acceptRequest(requestId, clientName, serviceName); break; } case 'request.decline': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; if (requestId && clientName) declineRequest(requestId, clientName); break; } case 'request.decline.confirm': { e.preventDefault(); const requestId = actionEl.getAttribute('data-request-id'); const clientName = actionEl.getAttribute('data-client-name'); if (!requestId || !clientName) return; const modal = bootstrap.Modal.getInstance(document.getElementById('declineConfirmModal')); if (modal) modal.hide(); showNotification(`Declining request from ${clientName}...`, 'info'); (async () => { try { await window.apiJson(`/api/dashboard/service-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: 'Declined', remove: true }) }); showNotification(`Request ${requestId} declined and removed`, 'warning'); if (window.dashboardInstance) { console.log('šŸ”„ Immediately refreshing dashboard data after decline...'); window.dashboardInstance.refreshServiceRequestsData(); } } catch (err) { console.error('Error declining request:', err); showNotification(`Failed to decline request: ${err.message}`, 'error'); } })(); break; } case 'service.manage': { e.preventDefault(); const serviceId = actionEl.dataset.serviceId; const serviceName = actionEl.dataset.serviceName; if (serviceId && serviceName) { showServiceManagementModal(serviceId, serviceName); } break; } case 'request.send_quote': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; if (requestId && clientName) sendQuote(requestId, clientName); break; } case 'request.complete': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const clientName = actionEl.dataset.clientName; if (requestId && clientName) completeRequest(requestId, clientName); break; } case 'request.complete.confirm': { e.preventDefault(); const requestId = actionEl.getAttribute('data-request-id'); const clientName = actionEl.getAttribute('data-client-name'); if (!requestId) return; const modal = bootstrap.Modal.getInstance(document.getElementById('completeConfirmModal')); if (modal) modal.hide(); showNotification(`Completing request ${requestId}...`, 'info'); (async () => { try { await window.apiJson(`/api/dashboard/service-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: 'Completed' }) }); showNotification(`Request ${requestId} completed successfully`, 'success'); if (window.dashboardInstance) { console.log('šŸ”„ Immediately refreshing dashboard data after complete...'); window.dashboardInstance.refreshServiceRequestsData(); } } catch (err) { console.error('Error completing request:', err); showNotification(`Failed to complete request: ${err.message}`, 'error'); } })(); break; } case 'request.view_completed': { e.preventDefault(); const requestId = actionEl.dataset.requestId; const serviceName = actionEl.dataset.serviceName; const clientName = actionEl.dataset.clientName; if (requestId) viewCompletedRequest(requestId, serviceName, clientName); break; } case 'request.view_report': { e.preventDefault(); const requestId = actionEl.dataset.requestId; showNotification(`Report for request ${requestId} is not implemented yet.`, 'info'); break; } case 'request.view_invoice': { e.preventDefault(); const requestId = actionEl.dataset.requestId; showNotification(`Invoice for request ${requestId} is not implemented yet.`, 'info'); break; } case 'request.progress.open': { e.preventDefault(); openProgressUpdateModal(); break; } case 'request.progress.save': { e.preventDefault(); saveProgressUpdate(); break; } case 'request.progress.back': { e.preventDefault(); backToRequestDetails(); break; } case 'sla.view': { e.preventDefault(); const slaId = actionEl.dataset.slaId; if (slaId) viewSLA(slaId); break; } case 'sla.create': { e.preventDefault(); createNewSLA(); break; } case 'sla.save': { e.preventDefault(); saveSLA(); break; } case 'sla.edit': { e.preventDefault(); const slaId = actionEl.dataset.slaId; if (slaId) editSLA(slaId); break; } case 'sla.saveChanges': { e.preventDefault(); const slaId = actionEl.dataset.slaId; if (slaId) saveSLAChanges(slaId); break; } case 'agreement.view': { e.preventDefault(); viewAgreement(); break; } case 'agreement.download': { e.preventDefault(); downloadAgreement(); break; } case 'agreement.requestChanges': { e.preventDefault(); requestChanges(); break; } case 'agreement.submitChangeRequest': { e.preventDefault(); submitChangeRequest(); break; } case 'services.create': { e.preventDefault(); createNewService(); break; } case 'services.saveChanges': { e.preventDefault(); if (typeof saveServiceChanges === 'function') { saveServiceChanges(); } break; } case 'pagination.change': { e.preventDefault(); const page = parseInt(actionEl.dataset.page || '1', 10); if (window.dashboardInstance) { window.dashboardInstance._currentCompletedPage = Math.max(1, page); window.dashboardInstance.populateClientRequests(); } break; } case 'availability.update': { e.preventDefault(); updateAvailability(); break; } default: break; } }); // Handle delegated change events (e.g., availability toggle) document.addEventListener('change', function(e) { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; if (action === 'availability.toggle') { toggleAvailability(); } }); // Handle delegated input events (e.g., progress slider live updates) document.addEventListener('input', function(e) { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; switch (action) { case 'request.progress.input': { updateProgressValue(e.target.value); break; } default: break; } }); // SLA Management Functions (moved from template for CSP compliance) function createNewSLA() { console.log('Creating new SLA'); const modalHtml = ` `; const existingModal = document.getElementById('createSLAModal'); if (existingModal) existingModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('createSLAModal')); modal.show(); } async function saveSLA() { const name = document.getElementById('slaName').value.trim(); const serviceType = document.getElementById('slaServiceType').value; const responseTime = document.getElementById('responseTime').value; const resolutionTime = document.getElementById('resolutionTime').value; const availabilityTarget = document.getElementById('availabilityTarget').value; if (!name || !serviceType || !responseTime || !resolutionTime || !availabilityTarget) { showNotification('Please fill in all required fields', 'error'); return; } // Backend expects snake_case keys like response_time_hours, resolution_time_hours, availability_percentage // Provide minimal required fields and sensible defaults for optional fields const slaData = { name, // Optional fields with defaults since create form doesn't capture them yet description: '', response_time_hours: parseInt(responseTime), resolution_time_hours: parseInt(resolutionTime), availability_percentage: parseFloat(availabilityTarget), support_hours: 'Business Hours', escalation_procedure: 'Standard escalation procedure', status: 'Active' }; try { await window.apiJson('/api/dashboard/slas', { method: 'POST', body: slaData }); { showNotification(`SLA "${name}" created successfully!`, 'success'); const modal = bootstrap.Modal.getInstance(document.getElementById('createSLAModal')); if (modal) modal.hide(); loadUserSLAs(); } } catch (error) { console.error('Error creating SLA:', error); showNotification('Error creating SLA: ' + error.message, 'error'); } } async function viewSLA(slaId) { console.log('Viewing SLA:', slaId); try { const data = await window.apiJson('/api/dashboard/slas'); const slas = Array.isArray(data) ? data : (data.slas || (data.data && data.data.slas)); if (!slas) { showNotification('Failed to load SLAs', 'error'); return; } const sla = slas.find(s => s.id === slaId); if (!sla) { showNotification('SLA not found', 'error'); return; } displaySLAModal(sla); } catch (error) { console.error('Error loading SLA:', error); showNotification('Error loading SLA: ' + error.message, 'error'); } } function displaySLAModal(sla) { const statusClass = sla.status === 'Active' ? 'success' : sla.status === 'Draft' ? 'secondary' : 'warning'; const modalHtml = ` `; const existingModal = document.getElementById('viewSLAModal'); if (existingModal) existingModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('viewSLAModal')); modal.show(); } async function editSLA(slaId) { console.log('Editing SLA:', slaId); try { const data = await window.apiJson('/api/dashboard/slas'); const slas = Array.isArray(data) ? data : (data.slas || (data.data && data.data.slas)); if (!slas) { showNotification('Failed to load SLA data', 'error'); return; } const sla = slas.find(s => s.id === slaId); if (!sla) { showNotification('SLA not found', 'error'); return; } const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewSLAModal')); if (viewModal) viewModal.hide(); showEditSLAModal(sla); } catch (error) { console.error('Error loading SLA for editing:', error); showNotification('Error loading SLA: ' + error.message, 'error'); } } function showEditSLAModal(sla) { const modalHtml = ` `; const existingModal = document.getElementById('editSLAModal'); if (existingModal) existingModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('editSLAModal')); modal.show(); } async function saveSLAChanges(slaId) { const name = document.getElementById('editSlaName').value.trim(); const description = document.getElementById('editSlaDescription').value.trim(); const responseTime = parseInt(document.getElementById('editResponseTime').value); const resolutionTime = parseInt(document.getElementById('editResolutionTime').value); const availability = parseFloat(document.getElementById('editAvailability').value); const supportHours = document.getElementById('editSupportHours').value; const escalation = document.getElementById('editEscalation').value.trim(); const status = document.getElementById('editStatus').value; if (!name) { showNotification('SLA name is required', 'error'); return; } const slaData = { name, description, response_time_hours: responseTime, resolution_time_hours: resolutionTime, availability_percentage: availability, support_hours: supportHours, escalation_procedure: escalation, status }; try { await window.apiJson(`/api/dashboard/slas/${encodeURIComponent(slaId)}`, { method: 'PUT', body: slaData }); showNotification('SLA updated successfully!', 'success'); const modal = bootstrap.Modal.getInstance(document.getElementById('editSLAModal')); if (modal) modal.hide(); loadUserSLAs(); } catch (error) { console.error('Error updating SLA:', error); showNotification('Error updating SLA: ' + error.message, 'error'); } } // Service Provider Agreement Functions (moved from template) function viewAgreement() { console.log('Viewing service provider agreement'); const modalHtml = ` `; const existingModal = document.getElementById('viewAgreementModal'); if (existingModal) existingModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('viewAgreementModal')); modal.show(); } function downloadAgreement() { const agreementContent = ` Service Provider Agreement - ThreeFold

ThreeFold Service Provider Agreement

Agreement Date: ${new Date().toLocaleDateString()}

Provider: Service Provider

Agreement ID: SPA-${Date.now()}

1. Service Provider Terms

This Service Provider Agreement ("Agreement") is entered into between ThreeFold and the Service Provider for the provision of services on the ThreeFold marketplace platform.

2. Service Obligations

The Service Provider agrees to:

3. Payment Terms

Payment for services will be processed through the ThreeFold platform using USD Credits. The Service Provider agrees to the platform's payment processing terms and fee structure.

4. Quality Standards

All services must meet ThreeFold's quality standards and client expectations. The Service Provider is responsible for maintaining a professional reputation and delivering high-quality work.

5. Dispute Resolution

Any disputes arising from this agreement will be resolved through ThreeFold's dispute resolution process, with mediation as the preferred method.

6. Termination

Either party may terminate this agreement with 30 days written notice. ThreeFold reserves the right to terminate immediately for violations of platform terms.

7. Agreement Acceptance

By using the ThreeFold service provider platform, you acknowledge that you have read, understood, and agree to be bound by the terms of this agreement.


ThreeFold Foundation

Date: ${new Date().toLocaleDateString()}


Service Provider Signature: _________________________

Date: _________________________

`; const printWindow = window.open('', '_blank'); printWindow.document.write(agreementContent); printWindow.document.close(); printWindow.onload = function() { printWindow.focus(); printWindow.print(); printWindow.onafterprint = function() { printWindow.close(); }; }; showNotification('Opening agreement for download/print...', 'success'); } function requestChanges() { console.log('Requesting agreement changes'); const modalHtml = ` `; const existingModal = document.getElementById('requestChangesModal'); if (existingModal) existingModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('requestChangesModal')); modal.show(); } function submitChangeRequest() { const changeType = document.getElementById('changeType').value; const description = document.getElementById('changeDescription').value.trim(); const reason = document.getElementById('changeReason').value.trim(); if (!changeType || !description || !reason) { showNotification('Please fill in all required fields', 'error'); return; } showNotification('Change request submitted successfully! You will receive a response within 5 business days.', 'success'); const modal = bootstrap.Modal.getInstance(document.getElementById('requestChangesModal')); if (modal) modal.hide(); } // Utility functions function loadPersistedServices() { // Load services from API instead of sessionStorage window.apiJson('/api/dashboard/services') .then(data => { if (data && data.services && data.services.length > 0) { console.log('Found persisted services:', data.services); } }) .catch(error => { console.error('Error loading persisted services:', error); }); } function loadAvailabilitySettings() { console.log('šŸ”§ Loading availability settings...'); window.apiJson('/api/dashboard/availability') .then(data => { console.log('šŸ”§ Availability API response:', data); if (data && data.availability) { const checkbox = document.getElementById('availabilityStatus'); const hoursInput = document.getElementById('availabilityHours'); if (checkbox) { checkbox.checked = data.availability.available; } if (hoursInput) { hoursInput.value = data.availability.weekly_hours; } } else { // Set default values if no data found const checkbox = document.getElementById('availabilityStatus'); const hoursInput = document.getElementById('availabilityHours'); if (checkbox) { checkbox.checked = true; } if (hoursInput) { hoursInput.value = 20; } } }) .catch(error => { console.error('šŸ”§ Error loading availability settings:', error); showNotification('Failed to load availability settings. Using defaults.', 'warning'); // Set default values on error const checkbox = document.getElementById('availabilityStatus'); const hoursInput = document.getElementById('availabilityHours'); if (checkbox) { checkbox.checked = true; } if (hoursInput) { hoursInput.value = 20; } }); } // Availability handlers function toggleAvailability() { const checkbox = document.getElementById('availabilityStatus'); if (!checkbox) return; const status = checkbox.checked ? 'Available' : 'Unavailable'; // Just a lightweight cue; saving happens via Update button showNotification(`Availability set to: ${status} (not saved)`, 'info'); } async function updateAvailability() { const checkbox = document.getElementById('availabilityStatus'); const hoursInput = document.getElementById('availabilityHours'); if (!checkbox || !hoursInput) return; const available = !!checkbox.checked; const hours = Number(hoursInput.value); if (!Number.isFinite(hours) || hours < 0 || hours > 168) { showNotification('Please enter valid weekly hours between 0 and 168.', 'error'); return; } const payload = { available, weekly_hours: Math.trunc(hours), updated_at: new Date().toISOString() }; try { await window.apiJson('/api/dashboard/availability', { method: 'PUT', body: payload }); showNotification(`Availability updated: ${available ? 'Available' : 'Unavailable'} @ ${payload.weekly_hours}h/week`, 'success'); } catch (err) { console.error('Error updating availability:', err); showNotification('Error updating availability: ' + err.message, 'error'); } } // Initialize availability on load document.addEventListener('DOMContentLoaded', function() { if (window.__serviceProviderAvailInit) return; window.__serviceProviderAvailInit = true; try { loadAvailabilitySettings(); } catch (e) { console.warn('loadAvailabilitySettings failed:', e); } }); // PHASE 1 FIX: Add the missing createNewService function async function createNewService() { console.log(' PHASE 1 FIX: createNewService called'); console.log('šŸ”§ PHASE 1 FIX: createNewService called'); // Get form elements const serviceName = document.getElementById('serviceName'); const serviceDesc = document.getElementById('serviceDesc'); const serviceCategory = document.getElementById('serviceCategory'); const serviceDelivery = document.getElementById('serviceDelivery'); const pricingType = document.getElementById('pricingType'); const priceAmount = document.getElementById('priceAmount'); const serviceExperience = document.getElementById('serviceExperience'); const availableHours = document.getElementById('availableHours'); const responseTime = document.getElementById('responseTime'); const serviceSkills = document.getElementById('serviceSkills'); // PHASE 1 FIX: Comprehensive form validation const validationErrors = []; if (!serviceName.value.trim()) { validationErrors.push('Service name is required'); serviceName.classList.add('is-invalid'); } else { serviceName.classList.remove('is-invalid'); } if (!serviceDesc.value.trim()) { validationErrors.push('Service description is required'); serviceDesc.classList.add('is-invalid'); } else { serviceDesc.classList.remove('is-invalid'); } if (!serviceCategory.value || serviceCategory.value === 'Select category') { validationErrors.push('Please select a service category'); serviceCategory.classList.add('is-invalid'); } else { serviceCategory.classList.remove('is-invalid'); } if (!serviceDelivery.value || serviceDelivery.value === 'Select delivery method') { validationErrors.push('Please select a delivery method'); serviceDelivery.classList.add('is-invalid'); } else { serviceDelivery.classList.remove('is-invalid'); } if (!pricingType.value || pricingType.value === 'Select pricing model') { validationErrors.push('Please select a pricing model'); pricingType.classList.add('is-invalid'); } else { pricingType.classList.remove('is-invalid'); } if (!priceAmount.value || parseFloat(priceAmount.value) <= 0) { validationErrors.push('Please enter a valid price amount'); priceAmount.classList.add('is-invalid'); } else { priceAmount.classList.remove('is-invalid'); } if (!serviceExperience.value || serviceExperience.value === 'Select experience level') { validationErrors.push('Please select an experience level'); serviceExperience.classList.add('is-invalid'); } else { serviceExperience.classList.remove('is-invalid'); } if (!availableHours.value || parseInt(availableHours.value) <= 0) { validationErrors.push('Please enter available hours per week'); availableHours.classList.add('is-invalid'); } else { availableHours.classList.remove('is-invalid'); } if (!responseTime.value || responseTime.value === 'Response time') { validationErrors.push('Please select a response time'); responseTime.classList.add('is-invalid'); } else { responseTime.classList.remove('is-invalid'); } // PHASE 1 FIX: Show validation errors if any if (validationErrors.length > 0) { showNotification(`Please fix the following errors:\n• ${validationErrors.join('\n• ')}`, 'error'); console.log('šŸ”§ PHASE 1 FIX: Validation failed:', validationErrors); return; } // PHASE 1 FIX: Show loading state const createBtn = document.getElementById('createServiceBtn'); const originalBtnText = createBtn.textContent; createBtn.disabled = true; createBtn.innerHTML = 'Creating...'; showNotification('Creating new service...', 'info'); try { // Create product/application data for marketplace (matching backend Product struct) const productData = { name: serviceName.value.trim(), description: serviceDesc.value.trim(), category: serviceCategory.value, price: parseFloat(priceAmount.value), currency: 'USD', // Default currency availability: 'Available', // Default availability attributes: { delivery_method: serviceDelivery.value, pricing_type: pricingType.value, experience_level: serviceExperience.value, available_hours: availableHours.value, response_time: responseTime.value, skills: serviceSkills.value.trim() || '' }, tags: serviceSkills.value.trim() ? serviceSkills.value.split(',').map(s => s.trim()) : [] }; console.log('šŸ”§ SERVICE PROVIDER: Sending product data:', productData); // Send POST request to create product (application) that will appear on marketplace const result = await window.apiJson('/api/dashboard/products', { method: 'POST', body: productData }); console.log('šŸ”§ SERVICE PROVIDER: API response data:', result); const data = (result && result.data) ? result.data : result; if (data) { // Success handling - product/application created showNotification(`Application "${productData.name}" created successfully and will appear on the Marketplace!`, 'success'); // Close modal and reset form const modal = bootstrap.Modal.getInstance(document.getElementById('createServiceModal')); if (modal) { modal.hide(); } // Reset form document.querySelector('#createServiceModal form').reset(); // Remove validation classes [serviceName, serviceDesc, serviceCategory, serviceDelivery, pricingType, priceAmount, serviceExperience, availableHours, responseTime, serviceSkills].forEach(element => { element.classList.remove('is-invalid'); }); // Emit global event for marketplace listeners if (typeof window.dispatchEvent === 'function') { const serviceCreatedEvent = new CustomEvent('serviceCreated', { detail: { product: data.product || data, type: 'application', source: 'service-provider-dashboard' } }); window.dispatchEvent(serviceCreatedEvent); console.log('šŸ”§ SERVICE PROVIDER: Emitted serviceCreated event for marketplace'); } // Refresh services list to show new application if (window.dashboardInstance) { console.log('šŸ”§ SERVICE PROVIDER: Refreshing services list...'); await window.dashboardInstance.populateServicesList(); // Also refresh charts to include new service data window.dashboardInstance.createServiceDistributionChart(); window.dashboardInstance.createServicePerformanceChart(); } console.log('šŸ”§ SERVICE PROVIDER: Application created successfully:', result); } else { throw new Error('Failed to create application'); } } catch (error) { console.error('šŸ”§ PHASE 1 FIX: Error creating service:', error); showNotification(`Failed to create service: ${error.message}`, 'error'); } finally { // PHASE 1 FIX: Restore button state createBtn.disabled = false; createBtn.textContent = originalBtnText; } } // PHASE 1 FIX: Add form validation helpers function validateServiceForm() { const requiredFields = [ 'serviceName', 'serviceDesc', 'serviceCategory', 'serviceDelivery', 'pricingType', 'priceAmount', 'serviceExperience', 'availableHours', 'responseTime' ]; let isValid = true; requiredFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (field) { const value = field.value.trim(); if (!value || value === 'Select category' || value === 'Select delivery method' || value === 'Select pricing model' || value === 'Select experience level' || value === 'Response time') { field.classList.add('is-invalid'); isValid = false; } else { field.classList.remove('is-invalid'); } } }); return isValid; } // PHASE 1 FIX: Add real-time validation document.addEventListener('DOMContentLoaded', function() { if (window.__serviceProviderValidationInit) return; window.__serviceProviderValidationInit = true; // Add event listeners for real-time validation const formFields = [ 'serviceName', 'serviceDesc', 'serviceCategory', 'serviceDelivery', 'pricingType', 'priceAmount', 'serviceExperience', 'availableHours', 'responseTime' ]; formFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (field) { field.addEventListener('input', function() { if (this.value.trim() && this.value !== 'Select category' && this.value !== 'Select delivery method' && this.value !== 'Select pricing model' && this.value !== 'Select experience level' && this.value !== 'Response time') { this.classList.remove('is-invalid'); } }); field.addEventListener('change', function() { if (this.value.trim() && this.value !== 'Select category' && this.value !== 'Select delivery method' && this.value !== 'Select pricing model' && this.value !== 'Select experience level' && this.value !== 'Response time') { this.classList.remove('is-invalid'); } }); } }); });