Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Client Appointments</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 scrollbar */ | |
::-webkit-scrollbar { | |
width: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
::-webkit-scrollbar-thumb { | |
background: #888; | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
/* Animation for notifications */ | |
@keyframes slideIn { | |
from { transform: translateX(100%); } | |
to { transform: translateX(0); } | |
} | |
@keyframes fadeOut { | |
from { opacity: 1; } | |
to { opacity: 0; } | |
} | |
.notification { | |
animation: slideIn 0.3s forwards, fadeOut 0.5s 2.5s forwards; | |
} | |
/* Custom checkbox */ | |
.custom-checkbox { | |
appearance: none; | |
-webkit-appearance: none; | |
width: 20px; | |
height: 20px; | |
border: 2px solid #4f46e5; | |
border-radius: 4px; | |
outline: none; | |
cursor: pointer; | |
position: relative; | |
} | |
.custom-checkbox:checked { | |
background-color: #4f46e5; | |
} | |
.custom-checkbox:checked::after { | |
content: '\2713'; | |
font-size: 14px; | |
color: white; | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen font-sans"> | |
<div class="container mx-auto px-4 py-6 max-w-md"> | |
<!-- Header --> | |
<header class="flex justify-between items-center mb-8"> | |
<h1 class="text-2xl font-bold text-indigo-700"> | |
<i class="fas fa-calendar-check mr-2"></i> Appointments | |
</h1> | |
<div class="flex space-x-2"> | |
<button id="calendarViewBtn" class="bg-gray-200 text-gray-700 p-2 rounded-full hover:bg-gray-300 transition"> | |
<i class="fas fa-calendar-alt"></i> | |
</button> | |
<button id="addBtn" class="bg-indigo-600 text-white p-2 rounded-full hover:bg-indigo-700 transition"> | |
<i class="fas fa-plus"></i> | |
</button> | |
</div> | |
</header> | |
<!-- Calendar View --> | |
<div id="calendarView" class="hidden mb-6 bg-white rounded-lg shadow p-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-lg font-medium" id="calendarMonthYear">Month Year</h3> | |
<div class="flex space-x-2"> | |
<button id="prevMonth" class="p-1 text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-chevron-left"></i> | |
</button> | |
<button id="todayBtn" class="px-2 py-1 text-sm bg-gray-100 rounded hover:bg-gray-200"> | |
Today | |
</button> | |
<button id="nextMonth" class="p-1 text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-chevron-right"></i> | |
</button> | |
</div> | |
</div> | |
<div class="grid grid-cols-7 gap-1 mb-2"> | |
<div class="text-center font-medium text-sm py-1">Sun</div> | |
<div class="text-center font-medium text-sm py-1">Mon</div> | |
<div class="text-center font-medium text-sm py-1">Tue</div> | |
<div class="text-center font-medium text-sm py-1">Wed</div> | |
<div class="text-center font-medium text-sm py-1">Thu</div> | |
<div class="text-center font-medium text-sm py-1">Fri</div> | |
<div class="text-center font-medium text-sm py-1">Sat</div> | |
</div> | |
<div id="calendarDays" class="grid grid-cols-7 gap-1"> | |
<!-- Calendar days will be generated here --> | |
</div> | |
</div> | |
<!-- Stats Cards --> | |
<div class="grid grid-cols-2 gap-4 mb-6"> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<p class="text-gray-500 text-sm">Today</p> | |
<h3 class="text-xl font-bold" id="todayCount">0</h3> | |
</div> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<p class="text-gray-500 text-sm">Upcoming</p> | |
<h3 class="text-xl font-bold" id="upcomingCount">0</h3> | |
</div> | |
</div> | |
<!-- Filter Tabs --> | |
<div class="flex mb-4 bg-white rounded-lg shadow overflow-hidden"> | |
<button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="all">All</button> | |
<button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="upcoming">Upcoming</button> | |
<button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="completed">Completed</button> | |
</div> | |
<!-- Search --> | |
<div class="relative mb-6"> | |
<input type="text" id="searchInput" placeholder="Search clients..." | |
class="w-full p-3 pl-10 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<i class="fas fa-search absolute left-3 top-3.5 text-gray-400"></i> | |
</div> | |
<!-- Appointments List --> | |
<div id="appointmentsList" class="space-y-3 max-h-[60vh] overflow-y-auto"> | |
<!-- Appointments will be loaded here --> | |
</div> | |
<!-- Empty State --> | |
<div id="emptyState" class="text-center py-10 hidden"> | |
<i class="fas fa-calendar-times text-4xl text-gray-300 mb-4"></i> | |
<h3 class="text-lg font-medium text-gray-500">No appointments found</h3> | |
<p class="text-gray-400">Add a new appointment by clicking the + button</p> | |
</div> | |
</div> | |
<!-- Add/Edit Appointment Modal --> | |
<div id="appointmentModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> | |
<div class="bg-white rounded-lg w-full max-w-md 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-medium" id="modalTitle">Add New Appointment</h3> | |
<button id="closeModal" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<form id="appointmentForm" class="p-4 space-y-4"> | |
<input type="hidden" id="appointmentId"> | |
<div> | |
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-1">Client Name</label> | |
<input type="text" id="clientName" required | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
</div> | |
<div> | |
<label for="clientPhone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label> | |
<input type="tel" id="clientPhone" | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
</div> | |
<div> | |
<label for="serviceType" class="block text-sm font-medium text-gray-700 mb-1">Service</label> | |
<select id="serviceType" required | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<option value="">Select a service</option> | |
<option value="FULL BODY">FULL BODY</option> | |
<option value="MAXI">MAXI</option> | |
<option value="MINI">MINI</option> | |
<option value="INGHINAL">INGHINAL</option> | |
<option value="AXILE">AXILE</option> | |
<option value="FACIAL">FACIAL</option> | |
</select> | |
</div> | |
<div> | |
<label for="appointmentDate" class="block text-sm font-medium text-gray-700 mb-1">Date</label> | |
<input type="date" id="appointmentDate" required | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
</div> | |
<div> | |
<label for="appointmentTime" class="block text-sm font-medium text-gray-700 mb-1">Time</label> | |
<input type="time" id="appointmentTime" required | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
</div> | |
<div> | |
<label for="appointmentNotes" class="block text-sm font-medium text-gray-700 mb-1">Notes</label> | |
<textarea id="appointmentNotes" rows="3" | |
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea> | |
</div> | |
<div class="flex items-center"> | |
<input type="checkbox" id="isCompleted" class="custom-checkbox mr-2"> | |
<label for="isCompleted" class="text-sm font-medium text-gray-700">Completed</label> | |
</div> | |
<div class="flex space-x-3 pt-2"> | |
<button type="submit" class="flex-1 bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition"> | |
Save | |
</button> | |
<button type="button" id="cancelBtn" class="flex-1 bg-gray-200 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-300 transition"> | |
Cancel | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
<!-- Notification --> | |
<div id="notification" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg hidden notification"> | |
<span id="notificationText">Appointment saved successfully!</span> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// DOM Elements | |
const addBtn = document.getElementById('addBtn'); | |
const appointmentModal = document.getElementById('appointmentModal'); | |
const closeModal = document.getElementById('closeModal'); | |
const cancelBtn = document.getElementById('cancelBtn'); | |
const appointmentForm = document.getElementById('appointmentForm'); | |
const appointmentsList = document.getElementById('appointmentsList'); | |
const emptyState = document.getElementById('emptyState'); | |
const searchInput = document.getElementById('searchInput'); | |
const filterTabs = document.querySelectorAll('.filter-tab'); | |
const todayCountEl = document.getElementById('todayCount'); | |
const upcomingCountEl = document.getElementById('upcomingCount'); | |
const notification = document.getElementById('notification'); | |
// Current filter state | |
let currentFilter = 'all'; | |
let currentSearch = ''; | |
// Initialize the app | |
initApp(); | |
// Event Listeners | |
addBtn.addEventListener('click', openAddModal); | |
calendarViewBtn.addEventListener('click', toggleCalendarView); | |
prevMonth.addEventListener('click', () => { | |
currentDate.setMonth(currentDate.getMonth() - 1); | |
renderCalendar(); | |
}); | |
nextMonth.addEventListener('click', () => { | |
currentDate.setMonth(currentDate.getMonth() + 1); | |
renderCalendar(); | |
}); | |
todayBtn.addEventListener('click', () => { | |
currentDate = new Date(); | |
renderCalendar(); | |
}); | |
closeModal.addEventListener('click', closeModalFunc); | |
cancelBtn.addEventListener('click', closeModalFunc); | |
appointmentForm.addEventListener('submit', saveAppointment); | |
searchInput.addEventListener('input', function(e) { | |
currentSearch = e.target.value.toLowerCase(); | |
renderAppointments(); | |
}); | |
filterTabs.forEach(tab => { | |
tab.addEventListener('click', function() { | |
filterTabs.forEach(t => t.classList.remove('bg-indigo-600', 'text-white')); | |
this.classList.add('bg-indigo-600', 'text-white'); | |
currentFilter = this.dataset.filter; | |
renderAppointments(); | |
}); | |
}); | |
// Set first tab as active | |
filterTabs[0].classList.add('bg-indigo-600', 'text-white'); | |
// Calendar variables | |
const calendarViewBtn = document.getElementById('calendarViewBtn'); | |
const calendarView = document.getElementById('calendarView'); | |
const prevMonth = document.getElementById('prevMonth'); | |
const nextMonth = document.getElementById('nextMonth'); | |
const todayBtn = document.getElementById('todayBtn'); | |
let currentDate = new Date(); | |
let currentView = 'list'; // 'list' or 'calendar' | |
// Functions | |
function initApp() { | |
// Initialize calendar | |
renderCalendar(); | |
// Check if appointments exist in localStorage | |
if (!localStorage.getItem('appointments')) { | |
// Add some sample data if empty | |
const sampleAppointments = [ | |
{ | |
id: Date.now().toString(), | |
clientName: 'John Doe', | |
clientPhone: '555-1234', | |
date: getFormattedDate(new Date()), | |
time: '10:00', | |
notes: 'First consultation', | |
completed: false, | |
createdAt: new Date().toISOString() | |
}, | |
{ | |
id: (Date.now() + 1).toString(), | |
clientName: 'Jane Smith', | |
clientPhone: '555-5678', | |
date: getFormattedDate(new Date(new Date().setDate(new Date().getDate() + 1))), | |
time: '14:30', | |
notes: 'Follow-up appointment', | |
completed: false, | |
createdAt: new Date().toISOString() | |
} | |
]; | |
localStorage.setItem('appointments', JSON.stringify(sampleAppointments)); | |
} | |
renderAppointments(); | |
updateStats(); | |
} | |
function openAddModal() { | |
document.getElementById('modalTitle').textContent = 'Add New Appointment'; | |
document.getElementById('appointmentId').value = ''; | |
document.getElementById('appointmentForm').reset(); | |
document.getElementById('serviceType').value = ''; | |
document.getElementById('isCompleted').checked = false; | |
// Set default date to today | |
const today = new Date(); | |
document.getElementById('appointmentDate').value = getFormattedDate(today); | |
// Set default time to next hour | |
const nextHour = today.getHours() + 1; | |
document.getElementById('appointmentTime').value = `${nextHour.toString().padStart(2, '0')}:00`; | |
appointmentModal.classList.remove('hidden'); | |
} | |
function openEditModal(appointment) { | |
document.getElementById('modalTitle').textContent = 'Edit Appointment'; | |
document.getElementById('appointmentId').value = appointment.id; | |
document.getElementById('clientName').value = appointment.clientName; | |
document.getElementById('clientPhone').value = appointment.clientPhone; | |
document.getElementById('serviceType').value = appointment.serviceType || ''; | |
document.getElementById('appointmentDate').value = appointment.date; | |
document.getElementById('appointmentTime').value = appointment.time; | |
document.getElementById('appointmentNotes').value = appointment.notes; | |
document.getElementById('isCompleted').checked = appointment.completed; | |
appointmentModal.classList.remove('hidden'); | |
} | |
function closeModalFunc() { | |
appointmentModal.classList.add('hidden'); | |
} | |
function saveAppointment(e) { | |
e.preventDefault(); | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const appointmentId = document.getElementById('appointmentId').value; | |
const appointment = { | |
id: appointmentId || Date.now().toString(), | |
clientName: document.getElementById('clientName').value, | |
clientPhone: document.getElementById('clientPhone').value, | |
serviceType: document.getElementById('serviceType').value, | |
date: document.getElementById('appointmentDate').value, | |
time: document.getElementById('appointmentTime').value, | |
notes: document.getElementById('appointmentNotes').value, | |
completed: document.getElementById('isCompleted').checked, | |
createdAt: appointmentId ? | |
(appointments.find(a => a.id === appointmentId)?.createdAt || new Date().toISOString()) : | |
new Date().toISOString() | |
}; | |
if (appointmentId) { | |
// Update existing appointment | |
const index = appointments.findIndex(a => a.id === appointmentId); | |
if (index !== -1) { | |
appointments[index] = appointment; | |
} | |
} else { | |
// Add new appointment | |
appointments.push(appointment); | |
} | |
localStorage.setItem('appointments', JSON.stringify(appointments)); | |
closeModalFunc(); | |
renderAppointments(); | |
updateStats(); | |
// Show notification | |
showNotification('Appointment saved successfully!'); | |
} | |
function deleteAppointment(id) { | |
if (confirm('Are you sure you want to delete this appointment?')) { | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const updatedAppointments = appointments.filter(a => a.id !== id); | |
localStorage.setItem('appointments', JSON.stringify(updatedAppointments)); | |
renderAppointments(); | |
updateStats(); | |
// Show notification | |
showNotification('Appointment deleted!'); | |
} | |
} | |
function toggleCompleted(id) { | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const appointment = appointments.find(a => a.id === id); | |
if (appointment) { | |
appointment.completed = !appointment.completed; | |
localStorage.setItem('appointments', JSON.stringify(appointments)); | |
renderAppointments(); | |
updateStats(); | |
// Show notification | |
showNotification(`Appointment marked as ${appointment.completed ? 'completed' : 'pending'}!`); | |
} | |
} | |
function renderAppointments() { | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
// Filter appointments based on current filter and search | |
let filteredAppointments = appointments.filter(appointment => { | |
// Apply search filter | |
const matchesSearch = currentSearch === '' || | |
appointment.clientName.toLowerCase().includes(currentSearch) || | |
appointment.clientPhone.includes(currentSearch) || | |
appointment.notes.toLowerCase().includes(currentSearch); | |
// Apply status filter | |
let matchesFilter = true; | |
if (currentFilter === 'upcoming') { | |
matchesFilter = !appointment.completed; | |
} else if (currentFilter === 'completed') { | |
matchesFilter = appointment.completed; | |
} | |
return matchesSearch && matchesFilter; | |
}); | |
// Sort appointments by date and time (upcoming first) | |
filteredAppointments.sort((a, b) => { | |
const dateA = new Date(`${a.date}T${a.time}`); | |
const dateB = new Date(`${b.date}T${b.time}`); | |
return dateA - dateB; | |
}); | |
// Clear the list | |
appointmentsList.innerHTML = ''; | |
if (filteredAppointments.length === 0) { | |
emptyState.classList.remove('hidden'); | |
} else { | |
emptyState.classList.add('hidden'); | |
filteredAppointments.forEach(appointment => { | |
const appointmentDate = new Date(`${appointment.date}T${appointment.time}`); | |
const isToday = isSameDay(new Date(), appointmentDate); | |
const isPast = appointmentDate < new Date() && !isToday; | |
const appointmentEl = document.createElement('div'); | |
appointmentEl.className = `bg-white rounded-lg shadow p-4 ${appointment.completed ? 'opacity-70' : ''} ${isPast && !appointment.completed ? 'border-l-4 border-red-500' : ''}`; | |
appointmentEl.innerHTML = ` | |
<div class="flex justify-between items-start mb-2"> | |
<div> | |
<h3 class="font-bold text-lg ${appointment.completed ? 'line-through' : ''}">${appointment.clientName}</h3> | |
<p class="text-gray-500 text-sm">${formatDate(appointment.date)} at ${appointment.time}</p> | |
</div> | |
<div class="flex space-x-2"> | |
<button class="toggle-complete p-1 text-${appointment.completed ? 'green' : 'gray'}-500" data-id="${appointment.id}"> | |
<i class="fas fa-${appointment.completed ? 'check-circle' : 'circle'}"></i> | |
</button> | |
<button class="edit-btn p-1 text-blue-500" data-id="${appointment.id}"> | |
<i class="fas fa-edit"></i> | |
</button> | |
<button class="delete-btn p-1 text-red-500" data-id="${appointment.id}"> | |
<i class="fas fa-trash"></i> | |
</button> | |
</div> | |
</div> | |
${appointment.clientPhone ? `<p class="text-gray-600 mb-1"><i class="fas fa-phone mr-2"></i>${appointment.clientPhone}</p>` : ''} | |
${appointment.serviceType ? `<p class="text-gray-600 mb-1"><i class="fas fa-cut mr-2"></i>${appointment.serviceType}</p>` : ''} | |
${appointment.notes ? `<p class="text-gray-600"><i class="fas fa-sticky-note mr-2"></i>${appointment.notes}</p>` : ''} | |
${isToday ? `<span class="inline-block mt-2 px-2 py-1 bg-indigo-100 text-indigo-800 text-xs rounded-full">Today</span>` : ''} | |
`; | |
appointmentsList.appendChild(appointmentEl); | |
}); | |
// Add event listeners to dynamically created buttons | |
document.querySelectorAll('.edit-btn').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const id = this.dataset.id; | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const appointment = appointments.find(a => a.id === id); | |
if (appointment) { | |
openEditModal(appointment); | |
} | |
}); | |
}); | |
document.querySelectorAll('.delete-btn').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const id = this.dataset.id; | |
deleteAppointment(id); | |
}); | |
}); | |
document.querySelectorAll('.toggle-complete').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const id = this.dataset.id; | |
toggleCompleted(id); | |
}); | |
}); | |
} | |
} | |
function toggleCalendarView() { | |
currentView = currentView === 'list' ? 'calendar' : 'list'; | |
if (currentView === 'calendar') { | |
appointmentsList.classList.add('hidden'); | |
calendarView.classList.remove('hidden'); | |
calendarViewBtn.classList.remove('bg-gray-200'); | |
calendarViewBtn.classList.add('bg-indigo-600', 'text-white'); | |
renderCalendar(); | |
} else { | |
appointmentsList.classList.remove('hidden'); | |
calendarView.classList.add('hidden'); | |
calendarViewBtn.classList.remove('bg-indigo-600', 'text-white'); | |
calendarViewBtn.classList.add('bg-gray-200'); | |
} | |
} | |
function renderCalendar() { | |
const year = currentDate.getFullYear(); | |
const month = currentDate.getMonth(); | |
// Update month/year display | |
const monthNames = ["January", "February", "March", "April", "May", "June", | |
"July", "August", "September", "October", "November", "December"]; | |
document.getElementById('calendarMonthYear').textContent = | |
`${monthNames[month]} ${year}`; | |
// Get first day of month and total days in month | |
const firstDay = new Date(year, month, 1).getDay(); | |
const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
// Get appointments for this month | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const monthAppointments = appointments.filter(appt => { | |
const apptDate = new Date(appt.date); | |
return apptDate.getFullYear() === year && apptDate.getMonth() === month; | |
}); | |
// Generate calendar days | |
const calendarDays = document.getElementById('calendarDays'); | |
calendarDays.innerHTML = ''; | |
// Add empty cells for days before first day of month | |
for (let i = 0; i < firstDay; i++) { | |
const emptyCell = document.createElement('div'); | |
emptyCell.className = 'h-16 p-1 border border-gray-100 bg-gray-50'; | |
calendarDays.appendChild(emptyCell); | |
} | |
// Add day cells | |
for (let day = 1; day <= daysInMonth; day++) { | |
const dayCell = document.createElement('div'); | |
dayCell.className = 'h-16 p-1 border border-gray-200 overflow-y-auto'; | |
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; | |
const dayAppointments = monthAppointments.filter(appt => appt.date === dateStr); | |
// Highlight today | |
const today = new Date(); | |
if (year === today.getFullYear() && month === today.getMonth() && day === today.getDate()) { | |
dayCell.classList.add('bg-indigo-50'); | |
} | |
// Day number | |
const dayNumber = document.createElement('div'); | |
dayNumber.className = 'text-right text-sm font-medium mb-1'; | |
dayNumber.textContent = day; | |
dayCell.appendChild(dayNumber); | |
// Appointments for this day | |
dayAppointments.forEach(appt => { | |
const apptEl = document.createElement('div'); | |
apptEl.className = `text-xs p-1 mb-1 rounded truncate ${appt.completed ? 'bg-green-100 text-green-800' : 'bg-indigo-100 text-indigo-800'}`; | |
apptEl.title = `${appt.clientName} - ${appt.time}`; | |
apptEl.textContent = `${appt.time} ${appt.clientName}`; | |
dayCell.appendChild(apptEl); | |
}); | |
calendarDays.appendChild(dayCell); | |
} | |
} | |
function updateStats() { | |
const appointments = JSON.parse(localStorage.getItem('appointments')) || []; | |
const today = new Date().toISOString().split('T')[0]; | |
const todayAppointments = appointments.filter(a => a.date === today && !a.completed).length; | |
const upcomingAppointments = appointments.filter(a => { | |
const appointmentDate = new Date(`${a.date}T${a.time}`); | |
return !a.completed && (new Date(a.date) > new Date() || | |
(isSameDay(new Date(), appointmentDate) && appointmentDate > new Date())); | |
}).length; | |
todayCountEl.textContent = todayAppointments; | |
upcomingCountEl.textContent = upcomingAppointments; | |
} | |
function showNotification(message) { | |
notificationText.textContent = message; | |
notification.classList.remove('hidden'); | |
// Hide after 3 seconds | |
setTimeout(() => { | |
notification.classList.add('hidden'); | |
}, 3000); | |
} | |
// Helper functions | |
function getFormattedDate(date) { | |
const year = date.getFullYear(); | |
const month = String(date.getMonth() + 1).padStart(2, '0'); | |
const day = String(date.getDate()).padStart(2, '0'); | |
return `${year}-${month}-${day}`; | |
} | |
function formatDate(dateString) { | |
const options = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }; | |
return new Date(dateString).toLocaleDateString(undefined, options); | |
} | |
function isSameDay(date1, date2) { | |
return date1.getFullYear() === date2.getFullYear() && | |
date1.getMonth() === date2.getMonth() && | |
date1.getDate() === date2.getDate(); | |
} | |
}); | |
</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/awesome-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |