Spaces:
Running
Running
<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> |