// 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
Manage
`;
}).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}
Review
${request.status === 'Quote Requested'
? `Send Quote ` :
`Accept
Decline `
}
`;
}).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}
${request.budget} TFP
${request.priority}
View
Complete
Update
`;
}).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))}
View
Report
Invoice
`;
}).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 = 'No SLA assigned ';
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 = `
SLA Details
Name: ${sla.name}
Description: ${sla.description || 'No description provided'}
Status: ${sla.status}
Response Time: ${sla.response_time_hours} hours
Resolution Time: ${sla.resolution_time_hours} hours
Performance Metrics
Availability Target: ${sla.availability_percentage}%
Support Hours: ${sla.support_hours}
Escalation: ${sla.escalation_procedure}
Created: ${sla.created_at}
SLA Terms
This SLA defines the service levels including response times of ${sla.response_time_hours} hours, resolution targets of ${sla.resolution_time_hours} hours, and availability commitments of ${sla.availability_percentage}%.
`;
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 = `
1. Agreement Overview
This Service Provider Agreement ("Agreement") is entered into between ThreeFold and the Service Provider for the provision of professional services through the ThreeFold marketplace platform.
2. Service Provider Obligations
Provide services with professional competence and in accordance with industry standards
Maintain appropriate certifications and qualifications
Respond to client requests within agreed timeframes
Maintain confidentiality of client information
3. Payment Terms
Payments will be processed through the ThreeFold platform using USD Credits. Service providers will receive payment upon successful completion of services as verified by the client.
4. Quality Standards
Service providers must maintain a minimum rating of 4.0 stars and respond to service requests within 24 hours unless otherwise specified in their service offerings.
5. Agreement Term
This agreement is effective from January 15, 2025, and will automatically renew on January 15, 2026, unless terminated by either party with 30 days written notice.
`;
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
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:
Provide services as described in their service listings
Maintain professional standards and quality
Respond to client requests within specified timeframes
Complete projects according to agreed specifications
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 = `
Type of Change
Select change type
Payment Terms
Service Level Requirements
Termination Clause
Liability Terms
Other
Description of Requested Changes
Reason for Change
`;
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');
}
});
}
});
});