This repository has been archived on 2025-12-01. You can view files and clone it, but cannot push or open issues or pull requests.
Files
projectmycelium_old/src/static/js/dashboard-service-provider.js
2025-09-01 21:37:01 -04:00

3374 lines
148 KiB
JavaScript

// 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 `
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-light p-2 rounded me-3">
<i class="bi bi-gear fs-4"></i>
</div>
<div>
<h6 class="mb-0">${service.name || 'Unnamed Service'}</h6>
<small class="text-muted">${service.category || 'General'}</small>
</div>
</div>
</td>
<td>${service.category || 'General'}</td>
<td><span class="badge bg-${statusClass}">${service.status || 'Active'}</span></td>
<td>${service.hourly_rate || service.price_per_hour || service.price_per_hour_usd || service.price_amount || 0} TFC/hour</td>
<td>${service.clients || 0}</td>
<td>
<div class="d-flex align-items-center">
<span class="me-2">${service.rating || 0}</span>
<div>${stars}</div>
</div>
</td>
<td>${service.total_hours || 0} hours</td>
<td>
<button class="btn btn-sm btn-primary" data-action="service.manage" data-service-id="${service.id}" data-service-name="${service.name}">
<i class="bi bi-gear me-1"></i>Manage
</button>
</td>
</tr>
`;
}).join('');
} else {
console.log('🔍 PHASE 2 FIX: No services found, showing empty message');
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted">
<div class="py-4">
<i class="bi bi-gear fs-1 text-muted mb-3"></i>
<p class="mb-0">No services available</p>
<small class="text-muted">Create your first service to get started</small>
</div>
</td>
</tr>
`;
}
}
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 = `
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Refresh Failed:</strong> Unable to load latest data. Please refresh the page.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 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 `
<tr>
<td><small>${request.id}</small></td>
<td>${request.client_name}</td>
<td>${request.service_name}</td>
<td><span class="badge bg-${statusClass}">${request.status}</span></td>
<td>${request.requested_date}</td>
<td>${request.estimated_hours}</td>
<td>${request.budget} TFP</td>
<td><span class="badge bg-${priorityClass}">${request.priority}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" data-action="request.view" data-request-id="${request.id}" data-client-name="${request.client_name}">Review</button>
${request.status === 'Quote Requested'
? `<button class="btn btn-sm btn-outline-info" data-action="request.send_quote" data-request-id="${request.id}" data-client-name="${request.client_name}">Send Quote</button>` :
`<button class="btn btn-sm btn-outline-success" data-action="request.accept" data-request-id="${request.id}" data-client-name="${request.client_name}" data-service-name="${request.service_name}">Accept</button>
<button class="btn btn-sm btn-outline-danger" data-action="request.decline" data-request-id="${request.id}" data-client-name="${request.client_name}">Decline</button>`
}
</div>
</td>
</tr>
`;
}).join('');
} else {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted">No open requests</td>
</tr>
`;
}
}
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 `
<tr>
<td><small>${request.id}</small></td>
<td>${request.client_name}</td>
<td>${request.service_name}</td>
<td><span class="badge bg-primary">${request.status}</span></td>
<td>${request.requested_date}</td>
<td>
<div class="progress" style="height: 20px; min-width: 80px;">
<div class="progress-bar ${progressBarClass}" role="progressbar" style="width: ${progress}%" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100">
${progress}%
</div>
</div>
</td>
<td>${request.budget} TFP</td>
<td><span class="badge bg-${priorityClass}">${request.priority}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" data-action="request.view" data-request-id="${request.id}" data-client-name="${request.client_name}">View</button>
<button class="btn btn-sm btn-outline-success" data-action="request.complete" data-request-id="${request.id}" data-client-name="${request.client_name}">Complete</button>
<button class="btn btn-sm btn-outline-info" data-action="request.update" data-request-id="${request.id}" data-client-name="${request.client_name}">Update</button>
</div>
</td>
</tr>
`;
}).join('');
} else {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted">No requests in progress</td>
</tr>
`;
}
}
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 `
<tr>
<td><small>${request.id}</small></td>
<td>${request.client_name}</td>
<td>${request.service_name}</td>
<td><span class="badge bg-success">${request.status}</span></td>
<td>${completedDate}</td>
<td>${totalHours}${typeof totalHours === 'number' ? ' hours' : ''}</td>
<td>${totalAmount}${typeof totalAmount === 'number' ? ' TFP' : ''}</td>
<td>
<div class="d-flex align-items-center">
<span class="me-2">${rating}</span>
<div>${this.generateStarRating(parseFloat(rating))}</div>
</div>
</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" data-action="request.view_completed" data-request-id="${request.id}" data-service-name="${request.service_name}" data-client-name="${request.client_name}">View</button>
<button class="btn btn-sm btn-outline-success" data-action="request.view_report" data-request-id="${request.id}">Report</button>
<button class="btn btn-sm btn-outline-info" data-action="request.view_invoice" data-request-id="${request.id}">Invoice</button>
</div>
</td>
</tr>
`;
}).join('');
} else {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted">No completed requests</td>
</tr>
`;
}
// 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 = `
<div class="d-flex justify-content-between align-items-center w-100">
<div class="text-muted">
Showing ${startItem}-${endItem} of ${totalItems} completed requests
</div>
<nav aria-label="Completed requests pagination">
<ul class="pagination pagination-sm mb-0">
`;
// Previous button
paginationHTML += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" data-action="pagination.change" data-page="${currentPage - 1}">Previous</a>
</li>
`;
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
paginationHTML += `<li class="page-item active"><a class="page-link" href="#">${i}</a></li>`;
} else if (i === 1 || i === totalPages || (i >= currentPage - 1 && i <= currentPage + 1)) {
paginationHTML += `<li class=\"page-item\"><a class=\"page-link\" href=\"#\" data-action=\"pagination.change\" data-page=\"${i}\">${i}</a></li>`;
} else if (i === currentPage - 2 || i === currentPage + 2) {
paginationHTML += `<li class="page-item disabled"><a class="page-link" href="#">...</a></li>`;
}
}
// Next button
paginationHTML += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" data-action="pagination.change" data-page="${currentPage + 1}">Next</a>
</li>
`;
paginationHTML += `
</ul>
</nav>
</div>
`;
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 += '<i class="bi bi-star-fill text-warning"></i>';
}
if (hasHalfStar) {
stars += '<i class="bi bi-star-half text-warning"></i>';
}
for (let i = 0; i < emptyStars; i++) {
stars += '<i class="bi bi-star text-warning"></i>';
}
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 `
<tr>
<td>
<div>
<strong>${client.name}</strong>
<br><small class="text-muted">${client.projects.length} projects</small>
</div>
</td>
<td>
<div>
${client.email ? `<div><i class="bi bi-envelope me-1"></i>${client.email}</div>` : ''}
${client.phone ? `<div><i class="bi bi-phone me-1"></i>${client.phone}</div>` : ''}
</div>
</td>
<td>${client.projects.length}</td>
<td>${client.total_revenue} TFP</td>
<td>
<div class="d-flex align-items-center">
<span class="me-2">${client.avg_rating.toFixed(1)}</span>
<div>${stars}</div>
</div>
</td>
<td><span class="badge bg-${statusClass}">${client.status}</span></td>
</tr>
`;
}).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 = '<option value="">No SLA assigned</option>';
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}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// 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 = `
<div class="text-center text-muted py-4">
<i class="bi bi-file-text fs-1 mb-3"></i>
<p class="mb-0">No SLAs created yet</p>
<small class="text-muted">Create your first SLA to get started</small>
</div>
`;
} else {
slaListGroup.innerHTML = slas.map(sla => {
const statusClass = sla.status === 'Active' ? 'primary' :
sla.status === 'Draft' ? 'secondary' : 'warning';
return `
<a href="#" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-action="sla.view" data-sla-id="${sla.id}">
<div>
<div class="fw-bold">${sla.name}</div>
<small class="text-muted">Response: ${sla.response_time_hours}h | Resolution: ${sla.resolution_time_hours}h | Availability: ${sla.availability_percentage}%</small>
</div>
<span class="badge bg-${statusClass} rounded-pill">${sla.status}</span>
</a>
`;
}).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 = `
<div class="modal fade" id="createSLAModal" tabindex="-1" aria-labelledby="createSLAModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createSLAModalLabel">Create New Service Level Agreement</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="createSLAForm">
<div class="mb-3">
<label for="slaName" class="form-label">SLA Name</label>
<input type="text" class="form-control" id="slaName" placeholder="e.g., Premium Support SLA">
</div>
<div class="mb-3">
<label for="slaServiceType" class="form-label">Service Type</label>
<select class="form-select" id="slaServiceType">
<option value="">Select service type</option>
<option value="System Administration">System Administration</option>
<option value="Custom Development">Custom Development</option>
<option value="Security Audit">Security Audit</option>
<option value="Migration Services">Migration Services</option>
<option value="Training">Training</option>
</select>
</div>
<div class="mb-3">
<label for="responseTime" class="form-label">Response Time (hours)</label>
<input type="number" class="form-control" id="responseTime" min="1" max="72" value="24">
</div>
<div class="mb-3">
<label for="resolutionTime" class="form-label">Resolution Time (hours)</label>
<input type="number" class="form-control" id="resolutionTime" min="1" max="168" value="48">
</div>
<div class="mb-3">
<label for="availabilityTarget" class="form-label">Availability Target (%)</label>
<input type="number" class="form-control" id="availabilityTarget" min="90" max="100" value="99">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-action="sla.save">Create SLA</button>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="modal fade" id="viewSLAModal" tabindex="-1" aria-labelledby="viewSLAModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewSLAModalLabel">${sla.name}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>SLA Details</h6>
<table class="table table-borderless">
<tr><td><strong>Name:</strong></td><td>${sla.name}</td></tr>
<tr><td><strong>Description:</strong></td><td>${sla.description || 'No description provided'}</td></tr>
<tr><td><strong>Status:</strong></td><td><span class="badge bg-${statusClass}">${sla.status}</span></td></tr>
<tr><td><strong>Response Time:</strong></td><td>${sla.response_time_hours} hours</td></tr>
<tr><td><strong>Resolution Time:</strong></td><td>${sla.resolution_time_hours} hours</td></tr>
</table>
</div>
<div class="col-md-6">
<h6>Performance Metrics</h6>
<table class="table table-borderless">
<tr><td><strong>Availability Target:</strong></td><td>${sla.availability_percentage}%</td></tr>
<tr><td><strong>Support Hours:</strong></td><td>${sla.support_hours}</td></tr>
<tr><td><strong>Escalation:</strong></td><td>${sla.escalation_procedure}</td></tr>
<tr><td><strong>Created:</strong></td><td>${sla.created_at}</td></tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>SLA Terms</h6>
<p class="text-muted">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}%.</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-action="sla.edit" data-sla-id="${sla.id}">Edit SLA</button>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="modal fade" id="editSLAModal" tabindex="-1" aria-labelledby="editSLAModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editSLAModalLabel">Edit SLA: ${sla.name}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editSLAForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editSlaName" class="form-label">SLA Name</label>
<input type="text" class="form-control" id="editSlaName" value="${sla.name}" required>
</div>
<div class="mb-3">
<label for="editSlaDescription" class="form-label">Description</label>
<textarea class="form-control" id="editSlaDescription" rows="3">${sla.description || ''}</textarea>
</div>
<div class="mb-3">
<label for="editResponseTime" class="form-label">Response Time (hours)</label>
<input type="number" class="form-control" id="editResponseTime" value="${sla.response_time_hours}" min="1" max="168" required>
</div>
<div class="mb-3">
<label for="editResolutionTime" class="form-label">Resolution Time (hours)</label>
<input type="number" class="form-control" id="editResolutionTime" value="${sla.resolution_time_hours}" min="1" max="720" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editAvailability" class="form-label">Availability Percentage</label>
<input type="number" class="form-control" id="editAvailability" value="${sla.availability_percentage}" min="90" max="100" step="0.1" required>
</div>
<div class="mb-3">
<label for="editSupportHours" class="form-label">Support Hours</label>
<select class="form-select" id="editSupportHours">
<option value="Business Hours" ${sla.support_hours === 'Business Hours' ? 'selected' : ''}>Business Hours (9 AM - 5 PM)</option>
<option value="Extended Hours" ${sla.support_hours === 'Extended Hours' ? 'selected' : ''}>Extended Hours (7 AM - 9 PM)</option>
<option value="24/7" ${sla.support_hours === '24/7' ? 'selected' : ''}>24/7 Support</option>
</select>
</div>
<div class="mb-3">
<label for="editEscalation" class="form-label">Escalation Procedure</label>
<textarea class="form-control" id="editEscalation" rows="3">${sla.escalation_procedure}</textarea>
</div>
<div class="mb-3">
<label for="editStatus" class="form-label">Status</label>
<select class="form-select" id="editStatus">
<option value="Active" ${sla.status === 'Active' ? 'selected' : ''}>Active</option>
<option value="Draft" ${sla.status === 'Draft' ? 'selected' : ''}>Draft</option>
<option value="Inactive" ${sla.status === 'Inactive' ? 'selected' : ''}>Inactive</option>
</select>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-action="sla.saveChanges" data-sla-id="${sla.id}">Save Changes</button>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="modal fade" id="viewAgreementModal" tabindex="-1" aria-labelledby="viewAgreementModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewAgreementModalLabel">ThreeFold Service Provider Agreement</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
<div class="agreement-content">
<h6>1. Agreement Overview</h6>
<p>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.</p>
<h6>2. Service Provider Obligations</h6>
<ul>
<li>Provide services with professional competence and in accordance with industry standards</li>
<li>Maintain appropriate certifications and qualifications</li>
<li>Respond to client requests within agreed timeframes</li>
<li>Maintain confidentiality of client information</li>
</ul>
<h6>3. Payment Terms</h6>
<p>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.</p>
<h6>4. Quality Standards</h6>
<p>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.</p>
<h6>5. Agreement Term</h6>
<p>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.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" data-action="agreement.download">Download PDF</button>
</div>
</div>
</div>
</div>
`;
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 = `
<!DOCTYPE html>
<html>
<head>
<title>Service Provider Agreement - ThreeFold</title>
<style>
@media print {
.no-print { display: none !important; }
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }
.agreement-content {
font-size: 12pt;
line-height: 1.4;
margin: 0;
padding: 0;
}
h1 { font-size: 18pt; margin-bottom: 20px; }
h2 { font-size: 14pt; margin-top: 20px; margin-bottom: 10px; }
h3 { font-size: 12pt; margin-top: 15px; margin-bottom: 8px; }
.header { text-align: center; margin-bottom: 30px; }
.section { margin-bottom: 20px; }
.signature-section { margin-top: 40px; }
}
@media screen {
body { font-family: Arial, sans-serif; margin: 20px; }
.agreement-content { max-width: 800px; margin: 0 auto; }
}
</style>
</head>
<body>
<div class="agreement-content">
<div class="header">
<h1>ThreeFold Service Provider Agreement</h1>
<p><strong>Agreement Date:</strong> ${new Date().toLocaleDateString()}</p>
<p><strong>Provider:</strong> Service Provider</p>
<p><strong>Agreement ID:</strong> SPA-${Date.now()}</p>
</div>
<div class="section">
<h2>1. Service Provider Terms</h2>
<p>This Service Provider Agreement ("Agreement") is entered into between ThreeFold and the Service Provider for the provision of services on the ThreeFold marketplace platform.</p>
</div>
<div class="section">
<h2>2. Service Obligations</h2>
<p>The Service Provider agrees to:</p>
<ul>
<li>Provide services as described in their service listings</li>
<li>Maintain professional standards and quality</li>
<li>Respond to client requests within specified timeframes</li>
<li>Complete projects according to agreed specifications</li>
</ul>
</div>
<div class="section">
<h2>3. Payment Terms</h2>
<p>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.</p>
</div>
<div class="section">
<h2>4. Quality Standards</h2>
<p>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.</p>
</div>
<div class="section">
<h2>5. Dispute Resolution</h2>
<p>Any disputes arising from this agreement will be resolved through ThreeFold's dispute resolution process, with mediation as the preferred method.</p>
</div>
<div class="section">
<h2>6. Termination</h2>
<p>Either party may terminate this agreement with 30 days written notice. ThreeFold reserves the right to terminate immediately for violations of platform terms.</p>
</div>
<div class="signature-section">
<h2>7. Agreement Acceptance</h2>
<p>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.</p>
<br>
<p><strong>ThreeFold Foundation</strong></p>
<p>Date: ${new Date().toLocaleDateString()}</p>
<br>
<p><strong>Service Provider Signature:</strong> _________________________</p>
<p>Date: _________________________</p>
</div>
</div>
</body>
</html>
`;
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 = `
<div class="modal fade" id="requestChangesModal" tabindex="-1" aria-labelledby="requestChangesModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="requestChangesModalLabel">Request Agreement Changes</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="requestChangesForm">
<div class="mb-3">
<label for="changeType" class="form-label">Type of Change</label>
<select class="form-select" id="changeType">
<option value="">Select change type</option>
<option value="payment_terms">Payment Terms</option>
<option value="service_levels">Service Level Requirements</option>
<option value="termination_clause">Termination Clause</option>
<option value="liability">Liability Terms</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<label for="changeDescription" class="form-label">Description of Requested Changes</label>
<textarea class="form-control" id="changeDescription" rows="4" placeholder="Please describe the changes you would like to request..."></textarea>
</div>
<div class="mb-3">
<label for="changeReason" class="form-label">Reason for Change</label>
<textarea class="form-control" id="changeReason" rows="3" placeholder="Please explain why this change is needed..."></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-action="agreement.submitChangeRequest">Submit Request</button>
</div>
</div>
</div>
</div>
`;
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 = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>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');
}
});
}
});
});