cobblerpro / index.html
kam33's picture
Add 3 files
24434b9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CobblerPro - Job Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom styles */
.slide-in {
animation: slideIn 0.3s forwards;
}
.slide-out {
animation: slideOut 0.3s forwards;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.to-do { background-color: #F59E0B; color: white; }
.in-progress { background-color: #3B82F6; color: white; }
.ready { background-color: #10B981; color: white; }
.completed { background-color: #64748B; color: white; }
.picked-up { background-color: #8B5CF6; color: white; }
.photo-placeholder {
background-color: #E5E7EB;
display: flex;
align-items: center;
justify-content: center;
color: #6B7280;
}
/* Swipe gestures */
.swipe-area {
touch-action: pan-y;
}
/* Custom scrollbar */
.custom-scroll::-webkit-scrollbar {
width: 6px;
}
.custom-scroll::-webkit-scrollbar-track {
background: #F1F5F9;
}
.custom-scroll::-webkit-scrollbar-thumb {
background: #CBD5E1;
border-radius: 3px;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="max-w-screen-md mx-auto bg-white min-h-screen shadow-lg overflow-hidden">
<!-- Header -->
<header class="bg-amber-800 text-white p-4 flex justify-between items-center sticky top-0 z-10">
<h1 class="text-2xl font-bold">CobblerPro</h1>
<div class="flex items-center space-x-4">
<button id="adminToggle" class="bg-amber-700 px-3 py-1 rounded-lg text-sm">
<i class="fas fa-user-shield mr-1"></i> Admin
</button>
<button id="historyBtn" class="bg-amber-700 px-3 py-1 rounded-lg text-sm">
<i class="fas fa-history mr-1"></i> History
</button>
</div>
</header>
<!-- Main Content Area -->
<main class="p-4 swipe-area" id="mainContent">
<!-- Dashboard View (Default) -->
<div id="dashboardView">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-800">Active Jobs</h2>
<button id="newJobBtn" class="bg-amber-600 hover:bg-amber-700 text-white px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-plus mr-2"></i> New Job
</button>
</div>
<!-- Status Filters -->
<div class="flex space-x-2 mb-4 overflow-x-auto pb-2">
<button class="status-filter px-3 py-1 rounded-full bg-gray-200 text-gray-800 text-sm whitespace-nowrap" data-status="all">All</button>
<button class="status-filter px-3 py-1 rounded-full bg-gray-200 text-gray-800 text-sm whitespace-nowrap" data-status="to-do">To Do</button>
<button class="status-filter px-3 py-1 rounded-full bg-gray-200 text-gray-800 text-sm whitespace-nowrap" data-status="in-progress">In Progress</button>
<button class="status-filter px-3 py-1 rounded-full bg-gray-200 text-gray-800 text-sm whitespace-nowrap" data-status="ready">Ready</button>
<button class="status-filter px-3 py-1 rounded-full bg-gray-200 text-gray-800 text-sm whitespace-nowrap" data-status="completed">Completed</button>
</div>
<!-- Search -->
<div class="relative mb-4">
<input type="text" id="jobSearch" placeholder="Search jobs..." class="w-full p-3 pl-10 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
<i class="fas fa-search absolute left-3 top-3.5 text-gray-400"></i>
</div>
<!-- Active Jobs Grid -->
<div id="activeJobsGrid" class="grid grid-cols-1 gap-4 mb-8">
<!-- Jobs will be loaded here dynamically -->
</div>
<!-- History Section (Collapsed by default) -->
<div id="historySection" class="hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800">Job History</h2>
<button id="hideHistoryBtn" class="text-amber-600 hover:text-amber-700">
<i class="fas fa-times"></i>
</button>
</div>
<div id="historyJobsList" class="space-y-3 custom-scroll max-h-96 overflow-y-auto">
<!-- History jobs will be loaded here dynamically -->
</div>
</div>
</div>
<!-- New Job - Service Type Selection -->
<div id="serviceTypeView" class="hidden slide-in">
<div class="flex items-center mb-6">
<button id="backToDashboard" class="mr-4 text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">New Job - Select Service Type</h2>
</div>
<div class="grid grid-cols-2 gap-4">
<button class="service-type-btn p-6 rounded-xl bg-amber-100 border-2 border-amber-300 flex flex-col items-center justify-center" data-type="repair">
<i class="fas fa-tools text-4xl text-amber-600 mb-2"></i>
<span class="text-lg font-medium">Repair</span>
</button>
<button class="service-type-btn p-6 rounded-xl bg-blue-100 border-2 border-blue-300 flex flex-col items-center justify-center" data-type="clean">
<i class="fas fa-broom text-4xl text-blue-600 mb-2"></i>
<span class="text-lg font-medium">Clean</span>
</button>
<button class="service-type-btn p-6 rounded-xl bg-purple-100 border-2 border-purple-300 flex flex-col items-center justify-center" data-type="shine">
<i class="fas fa-sparkles text-4xl text-purple-600 mb-2"></i>
<span class="text-lg font-medium">Shine</span>
</button>
<button class="service-type-btn p-6 rounded-xl bg-green-100 border-2 border-green-300 flex flex-col items-center justify-center" data-type="fit">
<i class="fas fa-shoe-prints text-4xl text-green-600 mb-2"></i>
<span class="text-lg font-medium">Fit</span>
</button>
</div>
</div>
<!-- New Job - Service Selection -->
<div id="serviceSelectionView" class="hidden slide-in">
<div class="flex items-center mb-6">
<button id="backToServiceType" class="mr-4 text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">Select Services</h2>
</div>
<div id="serviceList" class="mb-6">
<!-- Services will be loaded here dynamically -->
</div>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200 mb-4">
<div class="flex justify-between mb-2">
<span class="font-medium">Total Price:</span>
<span id="totalPrice" class="font-bold">$0</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Estimated Duration:</span>
<span id="totalDuration" class="font-bold">0 days</span>
</div>
</div>
<button id="proceedToCustomer" class="w-full bg-amber-600 hover:bg-amber-700 text-white py-3 rounded-lg font-medium">
Continue to Customer Details
</button>
</div>
<!-- New Job - Customer & Product Details -->
<div id="customerDetailsView" class="hidden slide-in">
<div class="flex items-center mb-6">
<button id="backToServiceSelection" class="mr-4 text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">Customer & Product Details</h2>
</div>
<div class="space-y-4">
<div>
<h3 class="font-medium text-gray-700 mb-2">Customer Information</h3>
<div class="space-y-3">
<div>
<label class="block text-sm text-gray-600 mb-1">Full Name *</label>
<input type="text" id="customerName" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500" required>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Phone Number *</label>
<input type="tel" id="customerPhone" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500" required>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Email (optional)</label>
<input type="email" id="customerEmail" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
</div>
</div>
<div>
<h3 class="font-medium text-gray-700 mb-2">Product Information</h3>
<div class="grid grid-cols-3 gap-3">
<div class="col-span-2">
<label class="block text-sm text-gray-600 mb-1">Brand</label>
<input type="text" id="productBrand" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Size</label>
<input type="text" id="productSize" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
</div>
<div class="mt-3">
<label class="block text-sm text-gray-600 mb-1">Color</label>
<input type="text" id="productColor" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
</div>
<div>
<h3 class="font-medium text-gray-700 mb-2">Before Photo</h3>
<div id="beforePhotoContainer" class="photo-placeholder w-full h-32 rounded-lg mb-2 cursor-pointer">
<div class="text-center">
<i class="fas fa-camera text-2xl mb-1"></i>
<p>Tap to add photo</p>
</div>
</div>
<input type="file" id="beforePhotoInput" accept="image/*" capture="camera" class="hidden">
</div>
<div>
<h3 class="font-medium text-gray-700 mb-2">Delivery & Payment</h3>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm text-gray-600 mb-1">Ready Date</label>
<input type="date" id="deliveryDate" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Payment Status</label>
<div class="relative">
<select id="paymentStatus" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500 appearance-none">
<option value="unpaid">Unpaid</option>
<option value="paid">Paid</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Notes</label>
<textarea id="jobNotes" rows="3" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500"></textarea>
</div>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
<h3 class="font-medium text-gray-700 mb-2">Job Summary</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>Service Type:</span>
<span id="summaryServiceType" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span>Services:</span>
<span id="summaryServices" class="font-medium text-right">-</span>
</div>
<div class="flex justify-between">
<span>Total Price:</span>
<span id="summaryTotalPrice" class="font-bold">$0</span>
</div>
<div class="flex justify-between">
<span>Duration:</span>
<span id="summaryDuration" class="font-medium">0 days</span>
</div>
</div>
</div>
<button id="saveJobBtn" class="w-full bg-amber-600 hover:bg-amber-700 text-white py-3 rounded-lg font-medium mt-4">
<i class="fas fa-save mr-2"></i> Save Job
</button>
</div>
</div>
<!-- Job Details View -->
<div id="jobDetailsView" class="hidden slide-in">
<div class="flex items-center mb-6">
<button id="backToDashboardFromDetails" class="mr-4 text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">Job Details</h2>
</div>
<div id="jobDetailsContent" class="space-y-4">
<!-- Job details will be loaded here dynamically -->
</div>
</div>
</main>
<!-- Admin Modal -->
<div id="adminModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20 hidden">
<div class="bg-white rounded-lg w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">Admin Panel</h3>
<button id="closeAdminModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4">
<h4 class="font-medium mb-2">Manage Services</h4>
<div class="mb-4">
<div class="flex items-center mb-2">
<select id="serviceCategorySelect" class="flex-1 p-2 border border-gray-300 rounded-l-lg">
<option value="repair">Repair</option>
<option value="clean">Clean</option>
<option value="shine">Shine</option>
<option value="fit">Fit</option>
</select>
<button id="addNewServiceBtn" class="bg-amber-600 text-white p-2 rounded-r-lg">
<i class="fas fa-plus"></i>
</button>
</div>
<div id="serviceManagementList" class="space-y-2">
<!-- Services for management will be loaded here -->
</div>
</div>
<div class="border-t border-gray-200 pt-4">
<h4 class="font-medium mb-2">Database Actions</h4>
<div class="grid grid-cols-2 gap-2">
<button id="exportDataBtn" class="bg-blue-100 text-blue-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-file-export mr-2"></i> Export
</button>
<button id="importDataBtn" class="bg-green-100 text-green-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-file-import mr-2"></i> Import
</button>
<button id="backupDataBtn" class="bg-purple-100 text-purple-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-database mr-2"></i> Backup
</button>
<button id="resetDataBtn" class="bg-red-100 text-red-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-trash-alt mr-2"></i> Reset
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Add/Edit Service Modal -->
<div id="serviceModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20 hidden">
<div class="bg-white rounded-lg w-full max-w-md mx-4">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold" id="serviceModalTitle">Add New Service</h3>
<button id="closeServiceModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4 space-y-3">
<input type="hidden" id="editServiceId">
<div>
<label class="block text-sm text-gray-600 mb-1">Service Name *</label>
<input type="text" id="serviceNameInput" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500" required>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm text-gray-600 mb-1">Price ($) *</label>
<input type="number" id="servicePriceInput" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500" required>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Duration (days) *</label>
<input type="number" id="serviceDurationInput" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500" required>
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Description</label>
<textarea id="serviceDescriptionInput" rows="2" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500"></textarea>
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end space-x-2">
<button id="cancelServiceBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100">
Cancel
</button>
<button id="saveServiceBtn" class="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700">
Save
</button>
<button id="deleteServiceBtn" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 hidden">
Delete
</button>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmationModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20 hidden">
<div class="bg-white rounded-lg w-full max-w-md mx-4">
<div class="p-4 border-b border-gray-200">
<h3 class="text-lg font-semibold" id="confirmationTitle">Confirm Action</h3>
</div>
<div class="p-4">
<p id="confirmationMessage">Are you sure you want to perform this action?</p>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end space-x-2">
<button id="cancelConfirmBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100">
Cancel
</button>
<button id="confirmActionBtn" class="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700">
Confirm
</button>
</div>
</div>
</div>
<!-- Status Toast -->
<div id="statusToast" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg flex items-center hidden z-30">
<span id="toastMessage"></span>
</div>
</div>
<script>
// Database setup
let db;
const DB_NAME = 'CobblerProDB';
const DB_VERSION = 1;
// Open or create IndexedDB database
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
db = event.target.result;
// Create object stores if they don't exist
if (!db.objectStoreNames.contains('jobs')) {
const jobsStore = db.createObjectStore('jobs', { keyPath: 'id', autoIncrement: true });
jobsStore.createIndex('status', 'status', { unique: false });
jobsStore.createIndex('customerName', 'customerName', { unique: false });
}
if (!db.objectStoreNames.contains('services')) {
const servicesStore = db.createObjectStore('services', { keyPath: 'id', autoIncrement: true });
servicesStore.createIndex('category', 'category', { unique: false });
// Add default services
const defaultServices = [
{ category: 'repair', name: 'Half Sole', price: 45, duration: 2, description: 'Replace half of the sole' },
{ category: 'repair', name: 'Full Sole Leather', price: 70, duration: 3, description: 'Replace full sole with leather' },
{ category: 'repair', name: 'Heel Guard', price: 25, duration: 1, description: 'Replace heel guard' },
{ category: 'clean', name: 'Basic Clean', price: 15, duration: 0, description: 'Basic cleaning service' },
{ category: 'clean', name: 'Deep Clean', price: 30, duration: 1, description: 'Deep cleaning with special solutions' },
{ category: 'shine', name: 'Standard Polish', price: 10, duration: 0, description: 'Standard shoe polish' },
{ category: 'shine', name: 'Premium Polish', price: 20, duration: 0, description: 'Premium polish with conditioning' },
{ category: 'fit', name: 'Stretching', price: 15, duration: 1, description: 'Shoe stretching service' },
{ category: 'fit', name: 'Sole Padding', price: 25, duration: 1, description: 'Add padding to sole' }
];
const transaction = event.target.transaction;
const store = transaction.objectStore('services');
defaultServices.forEach(service => {
store.add(service);
});
}
};
request.onsuccess = (event) => {
db = event.target.result;
loadActiveJobs();
updateServiceCounts();
};
request.onerror = (event) => {
console.error('Database error:', event.target.error);
showToast('Failed to initialize database', 'error');
};
// State management
let currentView = 'dashboard';
let selectedServiceType = '';
let selectedServices = [];
let currentJobId = null;
let isAdminMode = false;
let currentFilter = 'all';
// DOM elements
const mainContent = document.getElementById('mainContent');
const dashboardView = document.getElementById('dashboardView');
const serviceTypeView = document.getElementById('serviceTypeView');
const serviceSelectionView = document.getElementById('serviceSelectionView');
const customerDetailsView = document.getElementById('customerDetailsView');
const jobDetailsView = document.getElementById('jobDetailsView');
const activeJobsGrid = document.getElementById('activeJobsGrid');
const historySection = document.getElementById('historySection');
const historyJobsList = document.getElementById('historyJobsList');
const serviceList = document.getElementById('serviceList');
const totalPriceElement = document.getElementById('totalPrice');
const totalDurationElement = document.getElementById('totalDuration');
const jobSearch = document.getElementById('jobSearch');
// Buttons
const newJobBtn = document.getElementById('newJobBtn');
const historyBtn = document.getElementById('historyBtn');
const hideHistoryBtn = document.getElementById('hideHistoryBtn');
const adminToggle = document.getElementById('adminToggle');
const backToDashboard = document.getElementById('backToDashboard');
const backToServiceType = document.getElementById('backToServiceType');
const backToServiceSelection = document.getElementById('backToServiceSelection');
const backToDashboardFromDetails = document.getElementById('backToDashboardFromDetails');
const proceedToCustomer = document.getElementById('proceedToCustomer');
const saveJobBtn = document.getElementById('saveJobBtn');
// Service type buttons
const serviceTypeButtons = document.querySelectorAll('.service-type-btn');
// Status filter buttons
const statusFilters = document.querySelectorAll('.status-filter');
// Admin modal elements
const adminModal = document.getElementById('adminModal');
const closeAdminModal = document.getElementById('closeAdminModal');
const serviceCategorySelect = document.getElementById('serviceCategorySelect');
const addNewServiceBtn = document.getElementById('addNewServiceBtn');
const serviceManagementList = document.getElementById('serviceManagementList');
const exportDataBtn = document.getElementById('exportDataBtn');
const importDataBtn = document.getElementById('importDataBtn');
const backupDataBtn = document.getElementById('backupDataBtn');
const resetDataBtn = document.getElementById('resetDataBtn');
// Service modal elements
const serviceModal = document.getElementById('serviceModal');
const closeServiceModal = document.getElementById('closeServiceModal');
const serviceModalTitle = document.getElementById('serviceModalTitle');
const editServiceId = document.getElementById('editServiceId');
const serviceNameInput = document.getElementById('serviceNameInput');
const servicePriceInput = document.getElementById('servicePriceInput');
const serviceDurationInput = document.getElementById('serviceDurationInput');
const serviceDescriptionInput = document.getElementById('serviceDescriptionInput');
const saveServiceBtn = document.getElementById('saveServiceBtn');
const cancelServiceBtn = document.getElementById('cancelServiceBtn');
const deleteServiceBtn = document.getElementById('deleteServiceBtn');
// Confirmation modal elements
const confirmationModal = document.getElementById('confirmationModal');
const confirmationTitle = document.getElementById('confirmationTitle');
const confirmationMessage = document.getElementById('confirmationMessage');
const cancelConfirmBtn = document.getElementById('cancelConfirmBtn');
const confirmActionBtn = document.getElementById('confirmActionBtn');
// Toast element
const statusToast = document.getElementById('statusToast');
const toastMessage = document.getElementById('toastMessage');
// Event listeners
newJobBtn.addEventListener('click', () => {
showView('serviceType');
});
historyBtn.addEventListener('click', () => {
historySection.classList.remove('hidden');
loadHistoryJobs();
});
hideHistoryBtn.addEventListener('click', () => {
historySection.classList.add('hidden');
});
adminToggle.addEventListener('click', () => {
isAdminMode = !isAdminMode;
adminToggle.classList.toggle('bg-amber-700', isAdminMode);
adminToggle.classList.toggle('bg-gray-300', !isAdminMode);
showToast(isAdminMode ? 'Admin mode activated' : 'Admin mode deactivated');
});
backToDashboard.addEventListener('click', () => {
showView('dashboard');
});
backToServiceType.addEventListener('click', () => {
showView('serviceType');
});
backToServiceSelection.addEventListener('click', () => {
showView('serviceSelection');
});
backToDashboardFromDetails.addEventListener('click', () => {
showView('dashboard');
});
serviceTypeButtons.forEach(button => {
button.addEventListener('click', (e) => {
selectedServiceType = e.currentTarget.dataset.type;
loadServices(selectedServiceType);
showView('serviceSelection');
});
});
proceedToCustomer.addEventListener('click', () => {
if (selectedServices.length === 0) {
showToast('Please select at least one service', 'error');
return;
}
// Update summary
document.getElementById('summaryServiceType').textContent = selectedServiceType.charAt(0).toUpperCase() + selectedServiceType.slice(1);
document.getElementById('summaryServices').textContent = selectedServices.map(s => s.name).join(', ');
document.getElementById('summaryTotalPrice').textContent = calculateTotalPrice();
document.getElementById('summaryDuration').textContent = calculateTotalDuration() + ' days';
showView('customerDetails');
});
saveJobBtn.addEventListener('click', saveJob);
statusFilters.forEach(filter => {
filter.addEventListener('click', (e) => {
currentFilter = e.currentTarget.dataset.status;
// Update active filter button
statusFilters.forEach(f => {
f.classList.remove('bg-amber-600', 'text-white');
f.classList.add('bg-gray-200', 'text-gray-800');
});
e.currentTarget.classList.remove('bg-gray-200', 'text-gray-800');
e.currentTarget.classList.add('bg-amber-600', 'text-white');
loadActiveJobs();
});
});
jobSearch.addEventListener('input', () => {
loadActiveJobs();
});
// Admin modal events
adminToggle.addEventListener('dblclick', () => {
adminModal.classList.remove('hidden');
loadServicesForManagement(serviceCategorySelect.value);
});
closeAdminModal.addEventListener('click', () => {
adminModal.classList.add('hidden');
});
serviceCategorySelect.addEventListener('change', (e) => {
loadServicesForManagement(e.target.value);
});
addNewServiceBtn.addEventListener('click', () => {
openServiceModal('add');
});
// Service modal events
closeServiceModal.addEventListener('click', () => {
serviceModal.classList.add('hidden');
});
cancelServiceBtn.addEventListener('click', () => {
serviceModal.classList.add('hidden');
});
saveServiceBtn.addEventListener('click', saveService);
deleteServiceBtn.addEventListener('click', () => {
showConfirmation(
'Delete Service',
'Are you sure you want to delete this service? This action cannot be undone.',
() => {
const transaction = db.transaction(['services'], 'readwrite');
const store = transaction.objectStore('services');
store.delete(parseInt(editServiceId.value)).onsuccess = () => {
showToast('Service deleted successfully');
loadServicesForManagement(serviceCategorySelect.value);
serviceModal.classList.add('hidden');
// Reload services if we're in service selection view
if (currentView === 'serviceSelection') {
loadServices(selectedServiceType);
}
};
}
);
});
// Confirmation modal events
cancelConfirmBtn.addEventListener('click', () => {
confirmationModal.classList.add('hidden');
});
// Photo upload
document.getElementById('beforePhotoContainer').addEventListener('click', () => {
document.getElementById('beforePhotoInput').click();
});
document.getElementById('beforePhotoInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const photoContainer = document.getElementById('beforePhotoContainer');
photoContainer.innerHTML = '';
photoContainer.style.backgroundImage = `url(${event.target.result})`;
photoContainer.style.backgroundSize = 'cover';
photoContainer.style.backgroundPosition = 'center';
};
reader.readAsDataURL(file);
}
});
// Swipe gestures for navigation
let touchStartX = 0;
let touchEndX = 0;
mainContent.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, false);
mainContent.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, false);
function handleSwipe() {
const threshold = 50; // Minimum swipe distance
const swipeDistance = touchEndX - touchStartX;
if (Math.abs(swipeDistance) < threshold) return;
if (swipeDistance < 0 && currentView !== 'dashboard') {
// Swipe left (back)
switch(currentView) {
case 'serviceType':
showView('dashboard');
break;
case 'serviceSelection':
showView('serviceType');
break;
case 'customerDetails':
showView('serviceSelection');
break;
case 'jobDetails':
showView('dashboard');
break;
}
}
}
// Functions
function showView(view) {
// Hide all views first
dashboardView.classList.add('hidden');
serviceTypeView.classList.add('hidden');
serviceSelectionView.classList.add('hidden');
customerDetailsView.classList.add('hidden');
jobDetailsView.classList.add('hidden');
// Remove slide-in classes
serviceTypeView.classList.remove('slide-in');
serviceSelectionView.classList.remove('slide-in');
customerDetailsView.classList.remove('slide-in');
jobDetailsView.classList.remove('slide-in');
// Show the requested view
switch(view) {
case 'dashboard':
dashboardView.classList.remove('hidden');
loadActiveJobs();
break;
case 'serviceType':
serviceTypeView.classList.remove('hidden');
serviceTypeView.classList.add('slide-in');
break;
case 'serviceSelection':
serviceSelectionView.classList.remove('hidden');
serviceSelectionView.classList.add('slide-in');
break;
case 'customerDetails':
customerDetailsView.classList.remove('hidden');
customerDetailsView.classList.add('slide-in');
break;
case 'jobDetails':
jobDetailsView.classList.remove('hidden');
jobDetailsView.classList.add('slide-in');
break;
}
currentView = view;
}
function loadServices(category) {
const transaction = db.transaction(['services'], 'readonly');
const store = transaction.objectStore('services');
const index = store.index('category');
const request = index.getAll(category);
request.onsuccess = (e) => {
const services = e.target.result;
serviceList.innerHTML = '';
if (services.length === 0) {
serviceList.innerHTML = '<p class="text-gray-500 text-center py-4">No services found for this category</p>';
return;
}
services.forEach(service => {
const isSelected = selectedServices.some(s => s.id === service.id);
const serviceElement = document.createElement('div');
serviceElement.className = `p-4 rounded-lg border mb-2 cursor-pointer transition-colors ${isSelected ? 'bg-amber-100 border-amber-300' : 'bg-white border-gray-200 hover:bg-gray-50'}`;
serviceElement.dataset.id = service.id;
serviceElement.innerHTML = `
<div class="flex justify-between items-start">
<div>
<h3 class="font-medium">${service.name}</h3>
${service.description ? `<p class="text-sm text-gray-600 mt-1">${service.description}</p>` : ''}
</div>
<div class="text-right">
<span class="font-bold text-amber-700">$${service.price}</span>
<div class="text-xs text-gray-500 mt-1">${service.duration} day${service.duration !== 1 ? 's' : ''}</div>
</div>
</div>
${isAdminMode ? `<div class="flex justify-end mt-2">
<button class="edit-service-btn text-xs text-blue-600 hover:text-blue-800 mr-2" data-id="${service.id}">
<i class="fas fa-edit mr-1"></i> Edit
</button>
<button class="delete-service-btn text-xs text-red-600 hover:text-red-800" data-id="${service.id}">
<i class="fas fa-trash-alt mr-1"></i> Delete
</button>
</div>` : ''}
`;
serviceElement.addEventListener('click', () => {
toggleServiceSelection(service);
});
if (isAdminMode) {
const editBtn = serviceElement.querySelector('.edit-service-btn');
const deleteBtn = serviceElement.querySelector('.delete-service-btn');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
openServiceModal('edit', service);
});
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
showConfirmation(
'Delete Service',
`Are you sure you want to delete "${service.name}"?`,
() => {
const transaction = db.transaction(['services'], 'readwrite');
const store = transaction.objectStore('services');
store.delete(service.id).onsuccess = () => {
showToast('Service deleted successfully');
loadServices(category);
};
}
);
});
}
serviceList.appendChild(serviceElement);
});
};
}
function toggleServiceSelection(service) {
const index = selectedServices.findIndex(s => s.id === service.id);
if (index === -1) {
selectedServices.push(service);
} else {
selectedServices.splice(index, 1);
}
// Update UI
const serviceElement = document.querySelector(`[data-id="${service.id}"]`);
if (serviceElement) {
serviceElement.classList.toggle('bg-amber-100');
serviceElement.classList.toggle('border-amber-300');
}
// Update totals
totalPriceElement.textContent = calculateTotalPrice();
totalDurationElement.textContent = calculateTotalDuration() + ' days';
}
function calculateTotalPrice() {
return '$' + selectedServices.reduce((total, service) => total + service.price, 0);
}
function calculateTotalDuration() {
return selectedServices.reduce((total, service) => total + service.duration, 0);
}
function saveJob() {
const customerName = document.getElementById('customerName').value.trim();
const customerPhone = document.getElementById('customerPhone').value.trim();
const customerEmail = document.getElementById('customerEmail').value.trim();
const productBrand = document.getElementById('productBrand').value.trim();
const productColor = document.getElementById('productColor').value.trim();
const productSize = document.getElementById('productSize').value.trim();
const deliveryDate = document.getElementById('deliveryDate').value;
const paymentStatus = document.getElementById('paymentStatus').value;
const jobNotes = document.getElementById('jobNotes').value.trim();
// Validate required fields
if (!customerName || !customerPhone) {
showToast('Customer name and phone are required', 'error');
return;
}
if (selectedServices.length === 0) {
showToast('Please select at least one service', 'error');
return;
}
const job = {
customerName,
customerPhone,
customerEmail,
productBrand,
productColor,
productSize,
serviceType: selectedServiceType,
services: selectedServices,
totalPrice: selectedServices.reduce((total, service) => total + service.price, 0),
duration: selectedServices.reduce((total, service) => total + service.duration, 0),
deliveryDate,
paymentStatus,
jobNotes,
status: 'to-do',
pickupStatus: 'not-picked-up',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// Handle photo if uploaded
const photoInput = document.getElementById('beforePhotoInput');
if (photoInput.files.length > 0) {
const reader = new FileReader();
reader.onload = (e) => {
job.beforePhoto = e.target.result;
saveJobToDB(job);
};
reader.readAsDataURL(photoInput.files[0]);
} else {
saveJobToDB(job);
}
}
function saveJobToDB(job) {
const transaction = db.transaction(['jobs'], 'readwrite');
const store = transaction.objectStore('jobs');
const request = store.add(job);
request.onsuccess = () => {
showToast('Job saved successfully');
resetForm();
showView('dashboard');
loadActiveJobs();
};
request.onerror = () => {
showToast('Failed to save job', 'error');
};
}
function resetForm() {
// Reset form fields
document.getElementById('customerName').value = '';
document.getElementById('customerPhone').value = '';
document.getElementById('customerEmail').value = '';
document.getElementById('productBrand').value = '';
document.getElementById('productColor').value = '';
document.getElementById('productSize').value = '';
document.getElementById('deliveryDate').value = '';
document.getElementById('paymentStatus').value = 'unpaid';
document.getElementById('jobNotes').value = '';
// Reset photo
const photoContainer = document.getElementById('beforePhotoContainer');
photoContainer.innerHTML = `
<div class="text-center">
<i class="fas fa-camera text-2xl mb-1"></i>
<p>Tap to add photo</p>
</div>
`;
photoContainer.style.backgroundImage = '';
document.getElementById('beforePhotoInput').value = '';
// Reset service selection
selectedServices = [];
selectedServiceType = '';
}
function loadActiveJobs() {
const transaction = db.transaction(['jobs'], 'readonly');
const store = transaction.objectStore('jobs');
const request = store.getAll();
request.onsuccess = (e) => {
const allJobs = e.target.result;
let filteredJobs = allJobs;
// Filter by status if not 'all'
if (currentFilter !== 'all') {
filteredJobs = allJobs.filter(job => job.status === currentFilter);
}
// Filter by search term if any
const searchTerm = jobSearch.value.toLowerCase();
if (searchTerm) {
filteredJobs = filteredJobs.filter(job =>
job.customerName.toLowerCase().includes(searchTerm) ||
job.productBrand.toLowerCase().includes(searchTerm)
);
}
// Sort by creation date (newest first)
filteredJobs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// Display jobs
activeJobsGrid.innerHTML = '';
if (filteredJobs.length === 0) {
activeJobsGrid.innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-shoe-prints text-4xl mb-2"></i>
<p>No jobs found</p>
</div>
`;
return;
}
filteredJobs.forEach(job => {
const jobElement = document.createElement('div');
jobElement.className = 'p-4 rounded-lg border border-gray-200 bg-white hover:shadow-md transition-shadow cursor-pointer';
jobElement.dataset.id = job.id;
jobElement.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="font-medium">${job.customerName}</h3>
<p class="text-sm text-gray-600">${job.productBrand} ${job.productColor ? job.productColor : ''} ${job.productSize ? '· ' + job.productSize : ''}</p>
<div class="mt-2">
<span class="status-badge ${job.status.replace(' ', '-')}">${job.status.replace('-', ' ')}</span>
${job.paymentStatus === 'paid' ? '<span class="status-badge bg-green-100 text-green-800 ml-2">Paid</span>' : ''}
</div>
</div>
<div class="ml-4 flex flex-col items-end">
<span class="font-bold text-amber-700">$${job.totalPrice}</span>
<span class="text-xs text-gray-500">${job.duration} day${job.duration !== 1 ? 's' : ''}</span>
${job.beforePhoto ?
`<div class="w-16 h-16 rounded mt-2 bg-cover bg-center" style="background-image: url(${job.beforePhoto})"></div>` :
`<div class="w-16 h-16 rounded mt-2 bg-gray-200 flex items-center justify-center text-gray-400">
<i class="fas fa-shoe-prints"></i>
</div>`
}
</div>
</div>
`;
jobElement.addEventListener('click', () => {
viewJobDetails(job.id);
});
activeJobsGrid.appendChild(jobElement);
});
};
}
function loadHistoryJobs() {
const transaction = db.transaction(['jobs'], 'readonly');
const store = transaction.objectStore('jobs');
const request = store.getAll();
request.onsuccess = (e) => {
const allJobs = e.target.result;
// Filter completed or picked-up jobs
const historyJobs = allJobs.filter(job =>
job.status === 'completed' || job.pickupStatus === 'picked-up'
);
// Sort by completion/pickup date (newest first)
historyJobs.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
// Display history jobs
historyJobsList.innerHTML = '';
if (historyJobs.length === 0) {
historyJobsList.innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-history text-4xl mb-2"></i>
<p>No history jobs yet</p>
</div>
`;
return;
}
historyJobs.forEach(job => {
const jobElement = document.createElement('div');
jobElement.className = 'p-3 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 cursor-pointer';
jobElement.dataset.id = job.id;
// Calculate time ago
const updatedDate = new Date(job.updatedAt);
const now = new Date();
const diffHours = Math.floor((now - updatedDate) / (1000 * 60 * 60));
const diffMinutes = Math.floor((now - updatedDate) / (1000 * 60));
let timeAgo;
if (diffHours > 24) {
timeAgo = updatedDate.toLocaleDateString();
} else if (diffHours >= 1) {
timeAgo = `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
} else {
timeAgo = `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
}
jobElement.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="font-medium">${job.customerName}</h3>
<p class="text-sm text-gray-600">${job.services[0].name}${job.services.length > 1 ? ` +${job.services.length - 1} more` : ''}</p>
</div>
<div class="ml-4 flex flex-col items-end">
<span class="font-bold text-amber-700">$${job.totalPrice}</span>
<span class="text-xs text-gray-500">${timeAgo}</span>
<span class="status-badge ${job.pickupStatus === 'picked-up' ? 'picked-up' : 'completed'} mt-1">
${job.pickupStatus === 'picked-up' ? 'Picked Up' : 'Completed'}
</span>
</div>
</div>
`;
jobElement.addEventListener('click', () => {
viewJobDetails(job.id);
});
historyJobsList.appendChild(jobElement);
});
};
}
function viewJobDetails(jobId) {
const transaction = db.transaction(['jobs'], 'readonly');
const store = transaction.objectStore('jobs');
const request = store.get(parseInt(jobId));
request.onsuccess = (e) => {
const job = e.target.result;
if (!job) return;
currentJobId = job.id;
const jobDetailsContent = document.getElementById('jobDetailsContent');
jobDetailsContent.innerHTML = `
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold">${job.customerName}</h3>
<p class="text-gray-600">${job.customerPhone} ${job.customerEmail ? '· ' + job.customerEmail : ''}</p>
</div>
<div class="text-right">
<span class="font-bold text-amber-700 text-xl">$${job.totalPrice}</span>
<div class="text-sm text-gray-500">${job.duration} day${job.duration !== 1 ? 's' : ''}</div>
</div>
</div>
<div class="border-t border-gray-200 pt-3 mt-3">
<h4 class="font-medium text-gray-700 mb-2">Product Details</h4>
<p>${job.productBrand} ${job.productColor ? job.productColor : ''} ${job.productSize ? '· Size: ' + job.productSize : ''}</p>
</div>
<div class="border-t border-gray-200 pt-3 mt-3">
<h4 class="font-medium text-gray-700 mb-2">Services</h4>
<div class="space-y-2">
${job.services.map(service => `
<div class="flex justify-between">
<span>${service.name}</span>
<span>$${service.price}</span>
</div>
`).join('')}
</div>
</div>
${job.beforePhoto ? `
<div class="border-t border-gray-200 pt-3 mt-3">
<h4 class="font-medium text-gray-700 mb-2">Before Photo</h4>
<img src="${job.beforePhoto}" alt="Before photo" class="w-full rounded-lg">
</div>
` : ''}
${job.jobNotes ? `
<div class="border-t border-gray-200 pt-3 mt-3">
<h4 class="font-medium text-gray-700 mb-2">Notes</h4>
<p class="text-gray-700">${job.jobNotes}</p>
</div>
` : ''}
<div class="border-t border-gray-200 pt-3 mt-3">
<h4 class="font-medium text-gray-700 mb-2">Status</h4>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="block text-sm text-gray-600 mb-1">Job Status</label>
<div class="relative">
<select id="jobStatusSelect" class="w-full p-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500 appearance-none">
<option value="to-do" ${job.status === 'to-do' ? 'selected' : ''}>To Do</option>
<option value="in-progress" ${job.status === 'in-progress' ? 'selected' : ''}>In Progress</option>
<option value="ready" ${job.status === 'ready' ? 'selected' : ''}>Ready</option>
<option value="completed" ${job.status === 'completed' ? 'selected' : ''}>Completed</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Payment</label>
<div class="relative">
<select id="jobPaymentSelect" class="w-full p-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500 appearance-none">
<option value="unpaid" ${job.paymentStatus === 'unpaid' ? 'selected' : ''}>Unpaid</option>
<option value="paid" ${job.paymentStatus === 'paid' ? 'selected' : ''}>Paid</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Pickup</label>
<div class="relative">
<select id="jobPickupSelect" class="w-full p-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-amber-500 focus:border-amber-500 appearance-none">
<option value="not-picked-up" ${job.pickupStatus === 'not-picked-up' ? 'selected' : ''}>Not Picked Up</option>
<option value="picked-up" ${job.pickupStatus === 'picked-up' ? 'selected' : ''}>Picked Up</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 pt-3 mt-3">
<div class="grid grid-cols-3 gap-3">
<button id="editJobBtn" class="bg-blue-100 text-blue-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-edit mr-1"></i> Edit
</button>
<button id="shareJobBtn" class="bg-green-100 text-green-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-share-alt mr-1"></i> Share
</button>
<button id="archiveJobBtn" class="bg-red-100 text-red-700 p-2 rounded-lg flex items-center justify-center">
<i class="fas fa-archive mr-1"></i> Archive
</button>
</div>
</div>
</div>
`;
// Add event listeners for status changes
document.getElementById('jobStatusSelect').addEventListener('change', updateJobStatus);
document.getElementById('jobPaymentSelect').addEventListener('change', updateJobStatus);
document.getElementById('jobPickupSelect').addEventListener('change', updateJobStatus);
// Add event listeners for action buttons
document.getElementById('editJobBtn').addEventListener('click', () => {
showToast('Edit feature coming soon!');
});
document.getElementById('shareJobBtn').addEventListener('click', () => {
showToast('Share feature coming soon!');
});
document.getElementById('archiveJobBtn').addEventListener('click', () => {
showConfirmation(
'Archive Job',
'Are you sure you want to archive this job?',
() => {
const transaction = db.transaction(['jobs'], 'readwrite');
const store = transaction.objectStore('jobs');
store.delete(job.id).onsuccess = () => {
showToast('Job archived successfully');
showView('dashboard');
loadActiveJobs();
};
}
);
});
showView('jobDetails');
};
}
function updateJobStatus() {
const transaction = db.transaction(['jobs'], 'readwrite');
const store = transaction.objectStore('jobs');
const request = store.get(parseInt(currentJobId));
request.onsuccess = (e) => {
const job = e.target.result;
if (!job) return;
job.status = document.getElementById('jobStatusSelect').value;
job.paymentStatus = document.getElementById('jobPaymentSelect').value;
job.pickupStatus = document.getElementById('jobPickupSelect').value;
job.updatedAt = new Date().toISOString();
const updateRequest = store.put(job);
updateRequest.onsuccess = () => {
// Update the job list if we're on the dashboard
if (currentView === 'dashboard') {
loadActiveJobs();
}
};
};
}
function loadServicesForManagement(category) {
const transaction = db.transaction(['services'], 'readonly');
const store = transaction.objectStore('services');
const index = store.index('category');
const request = index.getAll(category);
request.onsuccess = (e) => {
const services = e.target.result;
serviceManagementList.innerHTML = '';
if (services.length === 0) {
serviceManagementList.innerHTML = '<p class="text-gray-500 text-center py-4">No services found for this category</p>';
return;
}
services.forEach(service => {
const serviceElement = document.createElement('div');
serviceElement.className = 'p-3 rounded-lg border border-gray-200 bg-white flex justify-between items-center';
serviceElement.innerHTML = `
<div>
<h4 class="font-medium">${service.name}</h4>
<p class="text-sm text-gray-600">$${service.price} · ${service.duration} day${service.duration !== 1 ? 's' : ''}</p>
</div>
<div class="flex space-x-2">
<button class="edit-service-btn text-blue-600 hover:text-blue-800" data-id="${service.id}">
<i class="fas fa-edit"></i>
</button>
<button class="delete-service-btn text-red-600 hover:text-red-800" data-id="${service.id}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
const editBtn = serviceElement.querySelector('.edit-service-btn');
const deleteBtn = serviceElement.querySelector('.delete-service-btn');
editBtn.addEventListener('click', () => {
openServiceModal('edit', service);
});
deleteBtn.addEventListener('click', () => {
showConfirmation(
'Delete Service',
`Are you sure you want to delete "${service.name}"?`,
() => {
const transaction = db.transaction(['services'], 'readwrite');
const store = transaction.objectStore('services');
store.delete(service.id).onsuccess = () => {
showToast('Service deleted successfully');
loadServicesForManagement(category);
};
}
);
});
serviceManagementList.appendChild(serviceElement);
});
};
}
function openServiceModal(mode, service = null) {
if (mode === 'add') {
serviceModalTitle.textContent = 'Add New Service';
editServiceId.value = '';
serviceNameInput.value = '';
servicePriceInput.value = '';
serviceDurationInput.value = '';
serviceDescriptionInput.value = '';
deleteServiceBtn.classList.add('hidden');
} else if (mode === 'edit' && service) {
serviceModalTitle.textContent = 'Edit Service';
editServiceId.value = service.id;
serviceNameInput.value = service.name;
servicePriceInput.value = service.price;
serviceDurationInput.value = service.duration;
serviceDescriptionInput.value = service.description || '';
deleteServiceBtn.classList.remove('hidden');
}
serviceModal.classList.remove('hidden');
}
function saveService() {
const name = serviceNameInput.value.trim();
const price = parseFloat(servicePriceInput.value);
const duration = parseInt(serviceDurationInput.value);
const description = serviceDescriptionInput.value.trim();
const category = serviceCategorySelect.value;
if (!name || isNaN(price) || isNaN(duration)) {
showToast('Please fill all required fields', 'error');
return;
}
const service = {
name,
price,
duration,
description,
category
};
const transaction = db.transaction(['services'], 'readwrite');
const store = transaction.objectStore('services');
if (editServiceId.value) {
// Update existing service
service.id = parseInt(editServiceId.value);
store.put(service).onsuccess = () => {
showToast('Service updated successfully');
loadServicesForManagement(category);
serviceModal.classList.add('hidden');
// Reload services if we're in service selection view
if (currentView === 'serviceSelection') {
loadServices(selectedServiceType);
}
};
} else {
// Add new service
store.add(service).onsuccess = () => {
showToast('Service added successfully');
loadServicesForManagement(category);
serviceModal.classList.add('hidden');
};
}
}
function showConfirmation(title, message, confirmCallback) {
confirmationTitle.textContent = title;
confirmationMessage.textContent = message;
confirmationModal.classList.remove('hidden');
// Remove previous event listeners
const newConfirmBtn = confirmActionBtn.cloneNode(true);
confirmActionBtn.parentNode.replaceChild(newConfirmBtn, confirmActionBtn);
newConfirmBtn.addEventListener('click', () => {
confirmationModal.classList.add('hidden');
confirmCallback();
});
}
function showToast(message, type = 'success') {
toastMessage.textContent = message;
statusToast.className = `fixed bottom-4 left-1/2 transform -translate-x-1/2 px-4 py-2 rounded-lg shadow-lg flex items-center z-30 ${type === 'error' ? 'bg-red-600' : 'bg-gray-800'} text-white`;
statusToast.classList.remove('hidden');
setTimeout(() => {
statusToast.classList.add('hidden');
}, 3000);
}
function updateServiceCounts() {
const transaction = db.transaction(['services'], 'readonly');
const store = transaction.objectStore('services');
const index = store.index('category');
const categories = ['repair', 'clean', 'shine', 'fit'];
categories.forEach(category => {
const countRequest = index.count(category);
countRequest.onsuccess = (e) => {
const count = e.target.result;
const button = document.querySelector(`.service-type-btn[data-type="${category}"]`);
if (button) {
const existingBadge = button.querySelector('.service-count-badge');
if (existingBadge) {
existingBadge.textContent = count;
} else {
const badge = document.createElement('span');
badge.className = 'service-count-badge absolute top-2 right-2 bg-white text-amber-600 text-xs font-bold px-2 py-1 rounded-full';
badge.textContent = count;
button.appendChild(badge);
button.style.position = 'relative';
}
}
};
});
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=kam33/cobblerpro" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>