Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>BuildTracker Pro - Construction Hours Logger</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js"></script> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --primary-dark: #2563eb; | |
| --secondary: #10b981; | |
| --secondary-dark: #059669; | |
| --accent: #f59e0b; | |
| --card: #ffffff; | |
| --card-dark: #1f2937; | |
| --text: #111827; | |
| --text-dark: #f9fafb; | |
| --muted: #6b7280; | |
| --muted-dark: #9ca3af; | |
| --border: #e5e7eb; | |
| --border-dark: #374151; | |
| --border-strong: #d1d5db; | |
| --border-strong-dark: #4b5563; | |
| --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| --shadow-dark: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f9fafb; | |
| color: var(--text); | |
| transition: all 0.3s ease; | |
| } | |
| body.dark { | |
| background-color: #111827; | |
| color: var(--text-dark); | |
| } | |
| .card { | |
| background-color: var(--card); | |
| border-radius: 12px; | |
| box-shadow: var(--shadow); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s ease; | |
| } | |
| .dark .card { | |
| background-color: var(--card-dark); | |
| box-shadow: var(--shadow-dark); | |
| border-color: var(--border-dark); | |
| } | |
| .btn { | |
| transition: all 0.2s ease; | |
| border-radius: 8px; | |
| font-weight: 500; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: white; | |
| border: none; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-dark); | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background-color: var(--secondary); | |
| color: white; | |
| border: none; | |
| } | |
| .btn-secondary:hover { | |
| background-color: var(--secondary-dark); | |
| transform: translateY(-1px); | |
| } | |
| .btn-danger { | |
| background-color: #ef4444; | |
| color: white; | |
| border: none; | |
| } | |
| .btn-danger:hover { | |
| background-color: #dc2626; | |
| transform: translateY(-1px); | |
| } | |
| .btn-ghost { | |
| background-color: transparent; | |
| color: var(--primary); | |
| border: 1px solid var(--primary); | |
| } | |
| .btn-ghost:hover { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| transform: translateY(-1px); | |
| } | |
| .dark .btn-ghost { | |
| color: #60a5fa; | |
| border-color: #60a5fa; | |
| } | |
| .dark .btn-ghost:hover { | |
| background-color: rgba(96, 165, 250, 0.1); | |
| } | |
| .input-field { | |
| border: 2px solid var(--border); | |
| border-radius: 8px; | |
| padding: 12px; | |
| transition: all 0.2s ease; | |
| background-color: white; | |
| } | |
| .input-field:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| .dark .input-field { | |
| background-color: #374151; | |
| border-color: var(--border-dark); | |
| color: white; | |
| } | |
| .dark .input-field:focus { | |
| border-color: #60a5fa; | |
| box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1); | |
| } | |
| .day-card { | |
| border: 2px solid var(--border-strong); | |
| border-radius: 10px; | |
| transition: all 0.2s ease; | |
| } | |
| .dark .day-card { | |
| border-color: var(--border-strong-dark); | |
| } | |
| .day-card:hover { | |
| border-color: var(--primary); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| } | |
| .dark .day-card:hover { | |
| border-color: #60a5fa; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| } | |
| .vanta-bg { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| } | |
| .glass-effect { | |
| background: rgba(255, 255, 255, 0.85); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .dark .glass-effect { | |
| background: rgba(17, 24, 39, 0.85); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .logo-container { | |
| background: linear-gradient(135deg, #3b82f6, #10b981); | |
| border-radius: 20px; | |
| padding: 4px; | |
| display: inline-block; | |
| } | |
| .logo-inner { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .dark .logo-inner { | |
| background: #1f2937; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <!-- Vanta.js Background --> | |
| <div id="vanta-bg" class="vanta-bg"></div> | |
| <!-- Notification Toast --> | |
| <div id="notification" class="fixed left-1/2 transform -translate-x-1/2 -top-16 transition-all duration-300 z-50 px-4 py-3 rounded-lg shadow-lg bg-white border border-gray-200 hidden"></div> | |
| <!-- Theme Toggle --> | |
| <div class="fixed top-4 right-4 flex items-center gap-2 z-50"> | |
| <div class="theme-toggle relative w-12 h-6 rounded-full bg-gray-200 cursor-pointer" id="themeToggle"> | |
| <div class="knob absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform"></div> | |
| </div> | |
| <span class="theme-toggle-label text-sm font-medium">Dark</span> | |
| </div> | |
| <!-- Auth Section --> | |
| <div id="authShell" class="min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div id="topLogo" class="mb-8"> | |
| <div class="logo-container"> | |
| <div class="logo-inner"> | |
| <img src="https://i.ibb.co/HLKfKpvC/DDFGTDGDsdfsdf-G.png" alt="BuildTracker Pro Logo" class="w-64 max-w-full"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Login Form --> | |
| <div id="loginSection" class="w-full max-w-md"> | |
| <div class="card p-8 glass-effect"> | |
| <h1 class="text-2xl font-bold text-center mb-6 text-gray-800 dark:text-white">Sign In</h1> | |
| <div class="space-y-4"> | |
| <div class="flex flex-col sm:flex-row gap-3"> | |
| <input type="text" id="firstName" placeholder="First name" class="input-field flex-1 min-w-0"> | |
| <input type="text" id="lastName" placeholder="Last name" class="input-field flex-1 min-w-0"> | |
| </div> | |
| <input type="password" id="pinUnique" placeholder="PIN" class="input-field w-full"> | |
| <button id="loginButton" class="btn btn-primary w-full py-3 rounded-lg font-medium">Login</button> | |
| <button id="createAccountButton" class="btn btn-ghost w-full py-2 text-sm">Create Account</button> | |
| <p class="text-center text-sm text-gray-500 dark:text-gray-400">Access limited to registered users</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Create Account Form --> | |
| <div id="createAccountSection" class="w-full max-w-md hidden"> | |
| <div class="card p-8 glass-effect"> | |
| <h1 class="text-2xl font-bold text-center mb-6 text-gray-800 dark:text-white">Create Account</h1> | |
| <div class="space-y-4"> | |
| <div class="flex flex-col sm:flex-row gap-3"> | |
| <input type="text" id="createAccountFirstNameUnique" placeholder="First name" class="input-field flex-1 min-w-0"> | |
| <input type="text" id="createAccountLastNameUnique" placeholder="Last name" class="input-field flex-1 min-w-0"> | |
| </div> | |
| <input type="password" id="createAccountPinUnique" placeholder="PIN (4+ digits)" class="input-field w-full"> | |
| <input type="password" id="createAccountRePinUnique" placeholder="Re-enter PIN" class="input-field w-full"> | |
| <button id="submitButton" class="btn btn-primary w-full py-3 rounded-lg font-medium">Submit</button> | |
| <button id="backButton" class="btn btn-danger w-full py-2 text-sm">Back</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main App Content --> | |
| <div id="mainContent" class="hidden container mx-auto p-4 max-w-6xl"> | |
| <!-- Impersonation Banner --> | |
| <div id="impersonationBanner" class="sticky top-0 z-40 bg-yellow-50 border-b border-yellow-300 text-yellow-800 p-3 hidden dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <span>Impersonating <strong id="impersonatingName"></strong></span> | |
| <button id="stopImpersonateBtn" class="btn btn-danger px-3 py-1 text-sm">Stop</button> | |
| </div> | |
| </div> | |
| <!-- Header --> | |
| <div class="text-center my-8"> | |
| <div class="logo-container inline-block"> | |
| <div class="logo-inner"> | |
| <img src="https://i.ibb.co/HLKfKpvC/DDFGTDGDsdfsdf-G.png" alt="BuildTracker Pro Logo" class="w-64 mx-auto"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Period Controls --> | |
| <div class="flex flex-col items-center mb-8"> | |
| <div class="card p-4 w-full max-w-2xl flex items-center justify-between glass-effect"> | |
| <select id="payPeriodSelect" class="input-field flex-1"></select> | |
| <button id="hamburgerBtn" class="ml-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"> | |
| <i data-feather="menu"></i> | |
| </button> | |
| </div> | |
| <div id="periodActionsRow" class="flex flex-wrap justify-center gap-3 mt-4"> | |
| <button id="menuEditPeriod" class="btn btn-ghost flex items-center gap-2"> | |
| <i data-feather="edit"></i> Edit | |
| </button> | |
| <button id="menuAddPeriod" class="btn btn-ghost flex items-center gap-2"> | |
| <i data-feather="plus"></i> New | |
| </button> | |
| <button id="menuRemovePeriods" class="btn btn-ghost flex items-center gap-2"> | |
| <i data-feather="trash-2"></i> Remove | |
| </button> | |
| <button id="menuDaysManager" class="btn btn-ghost flex items-center gap-2"> | |
| <i data-feather="calendar"></i> Days | |
| </button> | |
| <button id="openAdminBtn" class="btn btn-ghost flex items-center gap-2 hidden"> | |
| <i data-feather="shield"></i> Admin | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Period Editor --> | |
| <div id="dateEditContainer" class="card p-6 mb-8 max-w-2xl mx-auto glass-effect hidden"> | |
| <h4 class="text-lg font-semibold text-center mb-3 text-gray-800 dark:text-white">Pay Period Date Select</h4> | |
| <p class="text-sm text-gray-500 text-center mb-6">Select the start and end dates for the pay period</p> | |
| <div class="flex flex-wrap justify-center gap-6 mb-6"> | |
| <div> | |
| <label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Start</label> | |
| <input type="date" id="startDateInput" class="input-field"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">End</label> | |
| <input type="date" id="endDateInput" class="input-field"> | |
| </div> | |
| </div> | |
| <div class="flex justify-center gap-3"> | |
| <button id="savePeriodBtn" class="btn btn-primary px-6 py-2">Save</button> | |
| <button id="cancelPeriodBtn" class="btn btn-ghost px-6 py-2">Cancel</button> | |
| </div> | |
| </div> | |
| <!-- Main Content Sections --> | |
| <div id="dayTables" class="card p-6 mb-8 glass-effect"> | |
| <h3 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">Daily Time Tracking</h3> | |
| <!-- Day cards will be dynamically inserted here with visible borders --> | |
| </div> | |
| <div id="jobSummary" class="card p-6 mb-8 glass-effect"> | |
| <h3 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">Job Summary</h3> | |
| <!-- Job summary content will be dynamically inserted here --> | |
| </div> | |
| <div id="adminAggregate" class="card p-6 mb-8 glass-effect hidden"> | |
| <h3 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">Admin Overview</h3> | |
| <!-- Admin aggregate content will be dynamically inserted here --> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div id="mainActionRow" class="flex justify-center gap-4 mb-8 hidden"> | |
| <button id="generateInvoice" class="btn btn-primary px-6 py-3 flex items-center gap-2"> | |
| <i data-feather="file-text"></i> Generate Invoice | |
| </button> | |
| <button id="toggleLogs" class="btn btn-ghost px-6 py-3 flex items-center gap-2"> | |
| <i data-feather="list"></i> Show Logs | |
| </button> | |
| <button id="toggleMeta" class="btn btn-ghost px-6 py-3 flex items-center gap-2"> | |
| <i data-feather="info"></i> Show Meta Data | |
| </button> | |
| </div> | |
| <!-- Log and Meta Panels --> | |
| <div id="logPanel" class="card p-6 mb-8 max-h-48 overflow-auto glass-effect hidden"> | |
| <h4 class="text-lg font-semibold mb-3 text-gray-800 dark:text-white">Activity Log</h4> | |
| <!-- Log content will be dynamically inserted here --> | |
| </div> | |
| <div id="metaPanel" class="card p-6 mb-8 max-h-56 overflow-auto glass-effect hidden whitespace-pre"> | |
| <h4 class="text-lg font-semibold mb-3 text-gray-800 dark:text-white">Meta Data</h4> | |
| <!-- Meta data content will be dynamically inserted here --> | |
| </div> | |
| <!-- Footer --> | |
| <div class="text-center mt-12 mb-6"> | |
| <button id="footerLogout" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hidden flex items-center gap-2 mx-auto"> | |
| <i data-feather="log-out"></i> Logout | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Modals --> | |
| <div id="descModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="card p-6 max-w-md w-full mx-4 glass-effect"> | |
| <h2 class="text-xl font-bold text-center mb-4 text-gray-800 dark:text-white">Description</h2> | |
| <div id="descModalText" class="mb-4 break-words text-gray-700 dark:text-gray-300"></div> | |
| <div class="text-center"> | |
| <button id="closeDescModal" class="btn btn-ghost px-4 py-2">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="removePeriodsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="card p-6 max-w-md w-full mx-4 glass-effect"> | |
| <h3 class="text-lg font-bold text-center mb-2 text-gray-800 dark:text-white">Select Pay Periods to Delete</h3> | |
| <p class="text-sm text-gray-500 text-center mb-4">Check the box for any pay periods you want to remove</p> | |
| <div id="removePeriodsList" class="space-y-2 mb-4"></div> | |
| <div class="flex justify-center gap-3"> | |
| <button id="confirmRemovePeriods" class="btn btn-danger px-4 py-2">Delete</button> | |
| <button id="cancelRemovePeriods" class="btn btn-ghost px-4 py-2">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="daysWorkedModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="card p-6 max-w-md w-full mx-4 glass-effect"> | |
| <h2 class="text-xl font-bold text-center mb-4 text-gray-800 dark:text-white">Days Worked Manager</h2> | |
| <div id="daysWorkedList" class="mb-4"></div> | |
| <div class="flex justify-center gap-3"> | |
| <button id="saveDaysWorkedModal" class="btn btn-primary px-4 py-2">Save</button> | |
| <button id="closeDaysWorkedModal" class="btn btn-ghost px-4 py-2">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="adminModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="card p-6 w-full max-w-4xl glass-effect"> | |
| <h2 class="text-xl font-bold text-center mb-2 text-gray-800 dark:text-white">Admin Panel</h2> | |
| <p class="text-sm text-gray-500 text-center mb-4">Manage users, roles, and data</p> | |
| <div class="flex justify-center gap-3 mb-4"> | |
| <button id="adminCreateUser" class="btn btn-primary px-4 py-2 flex items-center gap-2"> | |
| <i data-feather="user-plus"></i> New User | |
| </button> | |
| <button id="adminRefresh" class="btn btn-ghost px-4 py-2 flex items-center gap-2"> | |
| <i data-feather="refresh-cw"></i> Refresh | |
| </button> | |
| <button id="adminClose" class="btn btn-danger px-4 py-2 flex items-center gap-2"> | |
| <i data-feather="x"></i> Close | |
| </button> | |
| </div> | |
| <div id="adminUsers" class="overflow-auto max-h-96"></div> | |
| </div> | |
| </div> | |
| <!-- Scripts --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.25/jspdf.plugin.autotable.min.js"></script> | |
| <script> | |
| // Initialize Vanta.js background | |
| let vantaEffect; | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize Vanta.js | |
| vantaEffect = VANTA.NET({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: 0x3b82f6, | |
| backgroundColor: 0xf9fafb, | |
| points: 12.00, | |
| maxDistance: 22.00, | |
| spacing: 18.00 | |
| }); | |
| // Initialize feather icons | |
| feather.replace(); | |
| // Set light theme by default | |
| document.documentElement.classList.remove('dark'); | |
| document.querySelector('.theme-toggle-label').textContent = 'Light'; | |
| // Theme toggle functionality | |
| document.getElementById('themeToggle').addEventListener('click', function() { | |
| const html = document.documentElement; | |
| const isDark = html.classList.toggle('dark'); | |
| document.querySelector('.theme-toggle-label').textContent = isDark ? 'Dark' : 'Light'; | |
| // Update Vanta.js background for theme | |
| if (vantaEffect) { | |
| vantaEffect.destroy(); | |
| } | |
| vantaEffect = VANTA.NET({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: isDark ? 0x60a5fa : 0x3b82f6, | |
| backgroundColor: isDark ? 0x111827 : 0xf9fafb, | |
| points: 12.00, | |
| maxDistance: 22.00, | |
| spacing: 18.00 | |
| }); | |
| // Update knob position | |
| const knob = document.querySelector('.theme-toggle .knob'); | |
| knob.style.transform = isDark ? 'translateX(24px)' : 'translateX(0)'; | |
| }); | |
| }); | |
| // Notification function | |
| function showNotification(message, type = 'info') { | |
| const notification = document.getElementById('notification'); | |
| notification.textContent = message; | |
| notification.className = `fixed left-1/2 transform -translate-x-1/2 top-4 transition-all duration-300 z-50 px-4 py-3 rounded-lg shadow-lg ${ | |
| type === 'error' ? 'bg-red-50 border-red-500 text-red-700' : | |
| type === 'success' ? 'bg-green-50 border-green-500 text-green-700' : | |
| 'bg-blue-50 border-blue-500 text-blue-700' | |
| }`; | |
| setTimeout(() => { | |
| notification.classList.add('hidden'); | |
| }, 3000); | |
| } | |
| // Example day card structure (for reference) | |
| function createDayCard(dayData) { | |
| return ` | |
| <div class="day-card p-4 mb-4"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h4 class="font-semibold text-lg">${dayData.date}</h4> | |
| <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-sm font-medium dark:bg-blue-900 dark:text-blue-200"> | |
| ${dayData.hours} hours | |
| </span> | |
| </div> | |
| <div class="space-y-2"> | |
| ${dayData.jobs.map(job => ` | |
| <div class="flex justify-between items-center p-2 bg-gray-50 rounded-lg dark:bg-gray-800"> | |
| <span class="font-medium">${job.name}</span> | |
| <span class="text-gray-600 dark:text-gray-400">${job.hours}h</span> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| </script> | |
| <!-- App scripts would be loaded here --> | |
| <script src="./js/globals.js"></script> | |
| <script src="./js/helpers.js"></script> | |
| <script src="./js/periods.js"></script> | |
| <script src="./js/data.js"></script> | |
| <script src="./js/admin_aggregate.js"></script> | |
| <script src="./js/render.js"></script> | |
| <script src="./js/job_actions.js"></script> | |
| <script src="./js/period_controls.js"></script> | |
| <script src="./js/invoice.js"></script> | |
| <script src="./js/auth_admin.js"></script> | |
| <script src="./js/init.js"></script> | |
| </body> | |
| </html> | |