|
<!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> |