| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Client 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> |
| .calendar-day { |
| min-height: 100px; |
| } |
| .calendar-day.today { |
| background-color: #f0f9ff; |
| } |
| .client-event { |
| font-size: 0.8rem; |
| border-left: 3px solid; |
| } |
| .client-event:hover { |
| transform: scale(1.02); |
| } |
| #addClientModal { |
| transition: all 0.3s ease; |
| } |
| .modal-open { |
| opacity: 1; |
| pointer-events: all; |
| } |
| .modal-closed { |
| opacity: 0; |
| pointer-events: none; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="flex flex-col md:flex-row justify-between items-center mb-8"> |
| <h1 class="text-3xl font-bold text-blue-600 mb-4 md:mb-0">Client Tracker</h1> |
| <div class="flex items-center space-x-4"> |
| <button id="prevMonth" class="p-2 rounded-full hover:bg-gray-200"> |
| <i class="fas fa-chevron-left"></i> |
| </button> |
| <h2 id="currentMonth" class="text-xl font-semibold">January 2023</h2> |
| <button id="nextMonth" class="p-2 rounded-full hover:bg-gray-200"> |
| <i class="fas fa-chevron-right"></i> |
| </button> |
| <button id="todayBtn" class="px-4 py-2 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200"> |
| Today |
| </button> |
| <button id="addClientBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> |
| <i class="fas fa-plus mr-2"></i>Add Client |
| </button> |
| </div> |
| </header> |
|
|
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| |
| <div class="grid grid-cols-7 bg-gray-100"> |
| <div class="py-2 text-center font-medium text-gray-500">Sun</div> |
| <div class="py-2 text-center font-medium text-gray-500">Mon</div> |
| <div class="py-2 text-center font-medium text-gray-500">Tue</div> |
| <div class="py-2 text-center font-medium text-gray-500">Wed</div> |
| <div class="py-2 text-center font-medium text-gray-500">Thu</div> |
| <div class="py-2 text-center font-medium text-gray-500">Fri</div> |
| <div class="py-2 text-center font-medium text-gray-500">Sat</div> |
| </div> |
| |
| |
| <div id="calendarDays" class="grid grid-cols-7 border-t border-l border-gray-200"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="mt-8 grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold text-gray-700 mb-2">Upcoming Appointments</h3> |
| <p id="upcomingCount" class="text-3xl font-bold text-blue-600">0</p> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold text-gray-700 mb-2">Total Clients</h3> |
| <p id="totalClients" class="text-3xl font-bold text-green-600">0</p> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold text-gray-700 mb-2">Recent Follow-ups</h3> |
| <p id="recentFollowups" class="text-3xl font-bold text-purple-600">0</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="addClientModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center modal-closed"> |
| <div class="bg-white rounded-lg p-6 w-full max-w-md"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-semibold">Add New Client</h3> |
| <button id="closeModal" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <form id="clientForm" class="space-y-4"> |
| <div> |
| <label for="clientName" class="block text-sm font-medium text-gray-700">Client Name</label> |
| <input type="text" id="clientName" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
| </div> |
| <div> |
| <label for="appointmentDate" class="block text-sm font-medium text-gray-700">Appointment Date</label> |
| <input type="date" id="appointmentDate" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
| </div> |
| <div> |
| <label for="appointmentTime" class="block text-sm font-medium text-gray-700">Appointment Time</label> |
| <input type="time" id="appointmentTime" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
| </div> |
| <div> |
| <label for="clientEmail" class="block text-sm font-medium text-gray-700">Email</label> |
| <input type="email" id="clientEmail" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
| </div> |
| <div> |
| <label for="clientPhone" class="block text-sm font-medium text-gray-700">Phone</label> |
| <input type="tel" id="clientPhone" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
| </div> |
| <div> |
| <label for="clientNotes" class="block text-sm font-medium text-gray-700">Notes</label> |
| <textarea id="clientNotes" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"></textarea> |
| </div> |
| <div class="flex justify-end space-x-3"> |
| <button type="button" id="cancelAddClient" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"> |
| Cancel |
| </button> |
| <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"> |
| Save Client |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div id="clientDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center modal-closed"> |
| <div class="bg-white rounded-lg p-6 w-full max-w-md"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 id="clientDetailsTitle" class="text-xl font-semibold">Client Details</h3> |
| <button id="closeDetailsModal" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div id="clientDetailsContent" class="space-y-3"> |
| |
| </div> |
| <div class="mt-6 flex justify-between"> |
| <button id="deleteClientBtn" class="px-4 py-2 bg-red-100 text-red-600 rounded-md hover:bg-red-200"> |
| Delete |
| </button> |
| <button id="editClientBtn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"> |
| Edit |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let currentDate = new Date(); |
| let clients = JSON.parse(localStorage.getItem('clients')) || []; |
| let selectedClientId = null; |
| |
| |
| const calendarDays = document.getElementById('calendarDays'); |
| const currentMonth = document.getElementById('currentMonth'); |
| const prevMonthBtn = document.getElementById('prevMonth'); |
| const nextMonthBtn = document.getElementById('nextMonth'); |
| const todayBtn = document.getElementById('todayBtn'); |
| const addClientBtn = document.getElementById('addClientBtn'); |
| const addClientModal = document.getElementById('addClientModal'); |
| const closeModal = document.getElementById('closeModal'); |
| const cancelAddClient = document.getElementById('cancelAddClient'); |
| const clientForm = document.getElementById('clientForm'); |
| const clientDetailsModal = document.getElementById('clientDetailsModal'); |
| const closeDetailsModal = document.getElementById('closeDetailsModal'); |
| const deleteClientBtn = document.getElementById('deleteClientBtn'); |
| const editClientBtn = document.getElementById('editClientBtn'); |
| const upcomingCount = document.getElementById('upcomingCount'); |
| const totalClients = document.getElementById('totalClients'); |
| const recentFollowups = document.getElementById('recentFollowups'); |
| |
| |
| prevMonthBtn.addEventListener('click', () => { |
| currentDate.setMonth(currentDate.getMonth() - 1); |
| renderCalendar(); |
| }); |
| |
| nextMonthBtn.addEventListener('click', () => { |
| currentDate.setMonth(currentDate.getMonth() + 1); |
| renderCalendar(); |
| }); |
| |
| todayBtn.addEventListener('click', () => { |
| currentDate = new Date(); |
| renderCalendar(); |
| }); |
| |
| addClientBtn.addEventListener('click', () => { |
| |
| const today = new Date(); |
| const formattedDate = today.toISOString().split('T')[0]; |
| document.getElementById('appointmentDate').value = formattedDate; |
| |
| |
| const nextHour = today.getHours() + 1; |
| document.getElementById('appointmentTime').value = `${nextHour.toString().padStart(2, '0')}:00`; |
| |
| addClientModal.classList.remove('modal-closed'); |
| addClientModal.classList.add('modal-open'); |
| }); |
| |
| closeModal.addEventListener('click', closeModals); |
| cancelAddClient.addEventListener('click', closeModals); |
| closeDetailsModal.addEventListener('click', closeModals); |
| |
| clientForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| addNewClient(); |
| }); |
| |
| deleteClientBtn.addEventListener('click', deleteClient); |
| editClientBtn.addEventListener('click', editClient); |
| |
| |
| function renderCalendar() { |
| |
| calendarDays.innerHTML = ''; |
| |
| |
| currentMonth.textContent = new Intl.DateTimeFormat('en-US', { |
| month: 'long', |
| year: 'numeric' |
| }).format(currentDate); |
| |
| |
| const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); |
| const lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); |
| const daysInMonth = lastDay.getDate(); |
| |
| |
| const startingDay = firstDay.getDay(); |
| |
| |
| const today = new Date(); |
| const isCurrentMonth = today.getFullYear() === currentDate.getFullYear() && |
| today.getMonth() === currentDate.getMonth(); |
| |
| |
| for (let i = 0; i < startingDay; i++) { |
| const dayElement = document.createElement('div'); |
| dayElement.className = 'calendar-day border-r border-b border-gray-200 p-2 bg-gray-50'; |
| calendarDays.appendChild(dayElement); |
| } |
| |
| |
| for (let day = 1; day <= daysInMonth; day++) { |
| const dayElement = document.createElement('div'); |
| dayElement.className = 'calendar-day border-r border-b border-gray-200 p-2'; |
| |
| |
| if (isCurrentMonth && day === today.getDate()) { |
| dayElement.classList.add('today'); |
| } |
| |
| |
| const dayNumber = document.createElement('div'); |
| dayNumber.className = 'text-right font-medium'; |
| dayNumber.textContent = day; |
| dayElement.appendChild(dayNumber); |
| |
| |
| const dayDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day); |
| const clientsForDay = getClientsForDate(dayDate); |
| |
| clientsForDay.forEach(client => { |
| const eventElement = document.createElement('div'); |
| eventElement.className = 'client-event mb-1 p-1 rounded bg-blue-50 border-l-blue-500 cursor-pointer hover:shadow'; |
| eventElement.innerHTML = ` |
| <div class="font-medium text-blue-700">${client.time}</div> |
| <div class="text-blue-600 truncate">${client.name}</div> |
| `; |
| eventElement.addEventListener('click', () => showClientDetails(client.id)); |
| dayElement.appendChild(eventElement); |
| }); |
| |
| calendarDays.appendChild(dayElement); |
| } |
| |
| updateStats(); |
| } |
| |
| function getClientsForDate(date) { |
| return clients.filter(client => { |
| const clientDate = new Date(client.date); |
| return clientDate.getFullYear() === date.getFullYear() && |
| clientDate.getMonth() === date.getMonth() && |
| clientDate.getDate() === date.getDate(); |
| }).sort((a, b) => a.time.localeCompare(b.time)); |
| } |
| |
| function addNewClient() { |
| const name = document.getElementById('clientName').value; |
| const date = document.getElementById('appointmentDate').value; |
| const time = document.getElementById('appointmentTime').value; |
| const email = document.getElementById('clientEmail').value; |
| const phone = document.getElementById('clientPhone').value; |
| const notes = document.getElementById('clientNotes').value; |
| |
| const newClient = { |
| id: Date.now().toString(), |
| name, |
| date, |
| time, |
| email, |
| phone, |
| notes, |
| createdAt: new Date().toISOString() |
| }; |
| |
| clients.push(newClient); |
| saveClients(); |
| renderCalendar(); |
| closeModals(); |
| clientForm.reset(); |
| } |
| |
| function showClientDetails(clientId) { |
| selectedClientId = clientId; |
| const client = clients.find(c => c.id === clientId); |
| |
| if (!client) return; |
| |
| document.getElementById('clientDetailsTitle').textContent = client.name; |
| |
| const detailsContent = document.getElementById('clientDetailsContent'); |
| detailsContent.innerHTML = ` |
| <div class="flex items-center"> |
| <i class="fas fa-calendar-day text-blue-500 mr-2"></i> |
| <div> |
| <div class="text-sm text-gray-500">Date</div> |
| <div>${formatDate(client.date)} at ${client.time}</div> |
| </div> |
| </div> |
| ${client.email ? ` |
| <div class="flex items-center"> |
| <i class="fas fa-envelope text-blue-500 mr-2"></i> |
| <div> |
| <div class="text-sm text-gray-500">Email</div> |
| <div>${client.email}</div> |
| </div> |
| </div>` : ''} |
| ${client.phone ? ` |
| <div class="flex items-center"> |
| <i class="fas fa-phone text-blue-500 mr-2"></i> |
| <div> |
| <div class="text-sm text-gray-500">Phone</div> |
| <div>${client.phone}</div> |
| </div> |
| </div>` : ''} |
| ${client.notes ? ` |
| <div class="flex items-start"> |
| <i class="fas fa-sticky-note text-blue-500 mr-2 mt-1"></i> |
| <div> |
| <div class="text-sm text-gray-500">Notes</div> |
| <div>${client.notes}</div> |
| </div> |
| </div>` : ''} |
| `; |
| |
| clientDetailsModal.classList.remove('modal-closed'); |
| clientDetailsModal.classList.add('modal-open'); |
| } |
| |
| function deleteClient() { |
| clients = clients.filter(client => client.id !== selectedClientId); |
| saveClients(); |
| renderCalendar(); |
| closeModals(); |
| } |
| |
| function editClient() { |
| const client = clients.find(c => c.id === selectedClientId); |
| if (!client) return; |
| |
| |
| document.getElementById('clientName').value = client.name; |
| document.getElementById('appointmentDate').value = client.date; |
| document.getElementById('appointmentTime').value = client.time; |
| document.getElementById('clientEmail').value = client.email || ''; |
| document.getElementById('clientPhone').value = client.phone || ''; |
| document.getElementById('clientNotes').value = client.notes || ''; |
| |
| |
| clientForm.removeEventListener('submit', addNewClient); |
| clientForm.addEventListener('submit', updateClient); |
| |
| |
| closeModals(); |
| addClientModal.classList.remove('modal-closed'); |
| addClientModal.classList.add('modal-open'); |
| } |
| |
| function updateClient(e) { |
| e.preventDefault(); |
| |
| const name = document.getElementById('clientName').value; |
| const date = document.getElementById('appointmentDate').value; |
| const time = document.getElementById('appointmentTime').value; |
| const email = document.getElementById('clientEmail').value; |
| const phone = document.getElementById('clientPhone').value; |
| const notes = document.getElementById('clientNotes').value; |
| |
| const updatedClient = { |
| id: selectedClientId, |
| name, |
| date, |
| time, |
| email, |
| phone, |
| notes, |
| createdAt: clients.find(c => c.id === selectedClientId).createdAt |
| }; |
| |
| clients = clients.map(client => |
| client.id === selectedClientId ? updatedClient : client |
| ); |
| |
| saveClients(); |
| renderCalendar(); |
| closeModals(); |
| clientForm.reset(); |
| |
| |
| clientForm.removeEventListener('submit', updateClient); |
| clientForm.addEventListener('submit', addNewClient); |
| } |
| |
| function closeModals() { |
| addClientModal.classList.remove('modal-open'); |
| addClientModal.classList.add('modal-closed'); |
| clientDetailsModal.classList.remove('modal-open'); |
| clientDetailsModal.classList.add('modal-closed'); |
| } |
| |
| function saveClients() { |
| localStorage.setItem('clients', JSON.stringify(clients)); |
| updateStats(); |
| } |
| |
| function formatDate(dateString) { |
| const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; |
| return new Date(dateString).toLocaleDateString('en-US', options); |
| } |
| |
| function updateStats() { |
| |
| const today = new Date(); |
| const nextWeek = new Date(today); |
| nextWeek.setDate(today.getDate() + 7); |
| |
| const upcoming = clients.filter(client => { |
| const clientDate = new Date(client.date); |
| return clientDate >= today && clientDate <= nextWeek; |
| }); |
| |
| upcomingCount.textContent = upcoming.length; |
| totalClients.textContent = clients.length; |
| |
| |
| const lastWeek = new Date(today); |
| lastWeek.setDate(today.getDate() - 7); |
| |
| const recent = clients.filter(client => { |
| const clientDate = new Date(client.createdAt); |
| return clientDate >= lastWeek && clientDate <= today; |
| }); |
| |
| recentFollowups.textContent = recent.length; |
| } |
| |
| |
| renderCalendar(); |
| </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=Cezarxil/app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |