Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Laser Hair Removal Salon Scheduler</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 that can't be done with Tailwind */ | |
.calendar-day:hover { | |
background-color: #f3f4f6; | |
cursor: pointer; | |
} | |
.calendar-day.has-appointments { | |
background-color: #e5e7eb; | |
} | |
.appointment-item:hover { | |
background-color: #f3f4f6; | |
} | |
#calendar { | |
min-height: 500px; | |
} | |
.fade-in { | |
animation: fadeIn 0.3s ease-in-out; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; } | |
to { opacity: 1; } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 font-sans"> | |
<!-- Login Screen (shown by default) --> | |
<div id="login-screen" class="fixed inset-0 flex items-center justify-center bg-gray-900 bg-opacity-50 z-50"> | |
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md"> | |
<div class="text-center mb-6"> | |
<i class="fas fa-spa text-4xl text-purple-600 mb-2"></i> | |
<h1 class="text-2xl font-bold text-gray-800">Laser Elegance Salon</h1> | |
<p class="text-gray-600">Staff Login</p> | |
</div> | |
<form id="login-form" class="space-y-4"> | |
<div> | |
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label> | |
<input type="text" id="username" name="username" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label> | |
<input type="password" id="password" name="password" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<button type="submit" | |
class="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"> | |
Login | |
</button> | |
</form> | |
</div> | |
</div> | |
<!-- Main App Container (hidden by default) --> | |
<div id="app-container" class="hidden"> | |
<!-- Header --> | |
<header class="bg-white shadow-sm"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center"> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-spa text-2xl text-purple-600"></i> | |
<h1 class="text-xl font-bold text-gray-800">Laser Elegance Scheduler</h1> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<button id="logout-btn" class="flex items-center text-gray-600 hover:text-purple-600"> | |
<i class="fas fa-sign-out-alt mr-1"></i> | |
<span>Logout</span> | |
</button> | |
</div> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
<!-- Tabs Navigation --> | |
<div class="border-b border-gray-200 mb-6"> | |
<nav class="-mb-px flex space-x-8"> | |
<button id="calendar-tab" class="tab-button active border-purple-500 text-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"> | |
<i class="fas fa-calendar-alt mr-2"></i>Calendar | |
</button> | |
<button id="appointments-tab" class="tab-button border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"> | |
<i class="fas fa-list mr-2"></i>Appointments | |
</button> | |
<button id="new-appointment-tab" class="tab-button border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"> | |
<i class="fas fa-plus-circle mr-2"></i>New Appointment | |
</button> | |
</nav> | |
</div> | |
<!-- Calendar Tab Content --> | |
<div id="calendar-content" class="tab-content fade-in"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-xl font-semibold text-gray-800">Appointment Calendar</h2> | |
<div class="flex items-center space-x-4"> | |
<button id="prev-month" class="p-2 rounded-full hover:bg-gray-200"> | |
<i class="fas fa-chevron-left"></i> | |
</button> | |
<span id="current-month" class="font-medium">July 2023</span> | |
<button id="next-month" class="p-2 rounded-full hover:bg-gray-200"> | |
<i class="fas fa-chevron-right"></i> | |
</button> | |
<button id="today-btn" class="px-3 py-1 bg-gray-200 rounded-md text-sm hover:bg-gray-300"> | |
Today | |
</button> | |
</div> | |
</div> | |
<div class="bg-white rounded-lg shadow overflow-hidden"> | |
<!-- Calendar Header (Days of Week) --> | |
<div class="grid grid-cols-7 bg-gray-100 border-b border-gray-200"> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Sun</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Mon</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Tue</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Wed</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Thu</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Fri</div> | |
<div class="py-2 text-center font-medium text-sm text-gray-600">Sat</div> | |
</div> | |
<!-- Calendar Grid --> | |
<div id="calendar" class="grid grid-cols-7 grid-rows-5 border border-gray-200 bg-white"> | |
<!-- Calendar days will be dynamically inserted here --> | |
</div> | |
</div> | |
<!-- Day Appointments Modal --> | |
<div id="day-appointments-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] overflow-y-auto"> | |
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> | |
<h3 id="modal-day-title" class="text-lg font-semibold text-gray-800">Appointments for July 15, 2023</h3> | |
<button id="close-day-modal" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div id="day-appointments-list" class="p-4 space-y-3"> | |
<!-- Appointments will be inserted here --> | |
</div> | |
<div class="px-6 py-3 border-t border-gray-200 flex justify-end"> | |
<button id="add-appointment-for-day" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"> | |
Add Appointment | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Appointments List Tab Content --> | |
<div id="appointments-content" class="tab-content hidden fade-in"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-xl font-semibold text-gray-800">Upcoming Appointments</h2> | |
<div class="flex items-center space-x-4"> | |
<div class="relative"> | |
<input type="text" id="search-appointments" placeholder="Search clients..." | |
class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
</div> | |
<button id="export-csv" class="px-3 py-1 bg-gray-200 rounded-md text-sm hover:bg-gray-300 flex items-center"> | |
<i class="fas fa-file-export mr-2"></i> Export CSV | |
</button> | |
</div> | |
</div> | |
<div class="bg-white rounded-lg shadow overflow-hidden"> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Client</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date & Time</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contact</th> | |
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> | |
</tr> | |
</thead> | |
<tbody id="appointments-table-body" class="bg-white divide-y divide-gray-200"> | |
<!-- Appointments will be inserted here --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
<!-- New Appointment Tab Content --> | |
<div id="new-appointment-content" class="tab-content hidden fade-in"> | |
<div class="bg-white rounded-lg shadow overflow-hidden"> | |
<div class="px-6 py-4 border-b border-gray-200"> | |
<h2 class="text-xl font-semibold text-gray-800">New Appointment</h2> | |
</div> | |
<div class="p-6"> | |
<form id="new-appointment-form" class="space-y-6"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
<div> | |
<label for="client-name" class="block text-sm font-medium text-gray-700 mb-1">Client Full Name *</label> | |
<input type="text" id="client-name" name="client-name" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="client-phone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number *</label> | |
<input type="tel" id="client-phone" name="client-phone" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="client-email" class="block text-sm font-medium text-gray-700 mb-1">Email (Optional)</label> | |
<input type="email" id="client-email" name="client-email" | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="service-package" class="block text-sm font-medium text-gray-700 mb-1">Service Package *</label> | |
<select id="service-package" name="service-package" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
<option value="">Select a package</option> | |
<option value="Full Body">Full Body</option> | |
<option value="Legs Only">Legs Only</option> | |
<option value="Underarms">Underarms</option> | |
<option value="Bikini Line">Bikini Line</option> | |
<option value="Brazilian">Brazilian</option> | |
<option value="Face">Face</option> | |
<option value="Back">Back</option> | |
<option value="Chest">Chest</option> | |
</select> | |
</div> | |
<div> | |
<label for="appointment-date" class="block text-sm font-medium text-gray-700 mb-1">Date *</label> | |
<input type="date" id="appointment-date" name="appointment-date" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="appointment-time" class="block text-sm font-medium text-gray-700 mb-1">Time *</label> | |
<input type="time" id="appointment-time" name="appointment-time" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
</div> | |
<div> | |
<label for="appointment-notes" class="block text-sm font-medium text-gray-700 mb-1">Notes (Optional)</label> | |
<textarea id="appointment-notes" name="appointment-notes" rows="3" | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"></textarea> | |
</div> | |
<div class="flex justify-end space-x-4"> | |
<button type="reset" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"> | |
Clear | |
</button> | |
<button type="submit" class="px-6 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"> | |
Schedule Appointment | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
<!-- Edit Appointment Modal --> | |
<div id="edit-appointment-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] overflow-y-auto"> | |
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> | |
<h3 class="text-lg font-semibold text-gray-800">Edit Appointment</h3> | |
<button id="close-edit-modal" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="p-6"> | |
<form id="edit-appointment-form" class="space-y-6"> | |
<input type="hidden" id="edit-appointment-id"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
<div> | |
<label for="edit-client-name" class="block text-sm font-medium text-gray-700 mb-1">Client Full Name *</label> | |
<input type="text" id="edit-client-name" name="edit-client-name" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="edit-client-phone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number *</label> | |
<input type="tel" id="edit-client-phone" name="edit-client-phone" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="edit-client-email" class="block text-sm font-medium text-gray-700 mb-1">Email (Optional)</label> | |
<input type="email" id="edit-client-email" name="edit-client-email" | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="edit-service-package" class="block text-sm font-medium text-gray-700 mb-1">Service Package *</label> | |
<select id="edit-service-package" name="edit-service-package" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
<option value="">Select a package</option> | |
<option value="Full Body">Full Body</option> | |
<option value="Legs Only">Legs Only</option> | |
<option value="Underarms">Underarms</option> | |
<option value="Bikini Line">Bikini Line</option> | |
<option value="Brazilian">Brazilian</option> | |
<option value="Face">Face</option> | |
<option value="Back">Back</option> | |
<option value="Chest">Chest</option> | |
</select> | |
</div> | |
<div> | |
<label for="edit-appointment-date" class="block text-sm font-medium text-gray-700 mb-1">Date *</label> | |
<input type="date" id="edit-appointment-date" name="edit-appointment-date" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
<div> | |
<label for="edit-appointment-time" class="block text-sm font-medium text-gray-700 mb-1">Time *</label> | |
<input type="time" id="edit-appointment-time" name="edit-appointment-time" required | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
</div> | |
</div> | |
<div> | |
<label for="edit-appointment-notes" class="block text-sm font-medium text-gray-700 mb-1">Notes (Optional)</label> | |
<textarea id="edit-appointment-notes" name="edit-appointment-notes" rows="3" | |
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"></textarea> | |
</div> | |
<div class="flex justify-end space-x-4"> | |
<button type="button" id="cancel-edit" 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-6 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"> | |
Update Appointment | |
</button> | |
<button type="button" id="delete-appointment" class="px-6 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"> | |
Delete | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
</main> | |
</div> | |
<script> | |
// Mock data for demonstration | |
let appointments = [ | |
{ | |
id: 1, | |
clientName: "Sarah Johnson", | |
phone: "555-123-4567", | |
email: "[email protected]", | |
service: "Full Body", | |
date: "2023-07-15", | |
time: "10:00", | |
notes: "First session, sensitive skin" | |
}, | |
{ | |
id: 2, | |
clientName: "Michael Chen", | |
phone: "555-987-6543", | |
email: "[email protected]", | |
service: "Back", | |
date: "2023-07-15", | |
time: "14:30", | |
notes: "Follow-up session" | |
}, | |
{ | |
id: 3, | |
clientName: "Emily Rodriguez", | |
phone: "555-456-7890", | |
email: "", | |
service: "Legs Only", | |
date: "2023-07-18", | |
time: "11:15", | |
notes: "" | |
}, | |
{ | |
id: 4, | |
clientName: "David Kim", | |
phone: "555-789-0123", | |
email: "[email protected]", | |
service: "Underarms", | |
date: "2023-07-20", | |
time: "09:00", | |
notes: "Allergic to aloe vera" | |
}, | |
{ | |
id: 5, | |
clientName: "Jessica Williams", | |
phone: "555-234-5678", | |
email: "[email protected]", | |
service: "Brazilian", | |
date: "2023-07-22", | |
time: "13:45", | |
notes: "Prefer cold gel" | |
} | |
]; | |
// Current user session | |
let currentUser = null; | |
// Current month and year for calendar | |
let currentDate = new Date(); | |
let currentMonth = currentDate.getMonth(); | |
let currentYear = currentDate.getFullYear(); | |
// DOM Elements | |
const loginScreen = document.getElementById('login-screen'); | |
const appContainer = document.getElementById('app-container'); | |
const loginForm = document.getElementById('login-form'); | |
const logoutBtn = document.getElementById('logout-btn'); | |
// Tab elements | |
const tabButtons = document.querySelectorAll('.tab-button'); | |
const tabContents = document.querySelectorAll('.tab-content'); | |
// Calendar elements | |
const calendar = document.getElementById('calendar'); | |
const currentMonthDisplay = document.getElementById('current-month'); | |
const prevMonthBtn = document.getElementById('prev-month'); | |
const nextMonthBtn = document.getElementById('next-month'); | |
const todayBtn = document.getElementById('today-btn'); | |
// Appointments list elements | |
const appointmentsTableBody = document.getElementById('appointments-table-body'); | |
const searchAppointments = document.getElementById('search-appointments'); | |
const exportCsvBtn = document.getElementById('export-csv'); | |
// New appointment form elements | |
const newAppointmentForm = document.getElementById('new-appointment-form'); | |
// Modal elements | |
const dayAppointmentsModal = document.getElementById('day-appointments-modal'); | |
const modalDayTitle = document.getElementById('modal-day-title'); | |
const dayAppointmentsList = document.getElementById('day-appointments-list'); | |
const closeDayModal = document.getElementById('close-day-modal'); | |
const addAppointmentForDay = document.getElementById('add-appointment-for-day'); | |
const editAppointmentModal = document.getElementById('edit-appointment-modal'); | |
const editAppointmentForm = document.getElementById('edit-appointment-form'); | |
const closeEditModal = document.getElementById('close-edit-modal'); | |
const cancelEdit = document.getElementById('cancel-edit'); | |
const deleteAppointmentBtn = document.getElementById('delete-appointment'); | |
// Event Listeners | |
document.addEventListener('DOMContentLoaded', () => { | |
// Initialize the app | |
if (localStorage.getItem('laserSalonUser')) { | |
currentUser = JSON.parse(localStorage.getItem('laserSalonUser')); | |
loginScreen.classList.add('hidden'); | |
appContainer.classList.remove('hidden'); | |
renderCalendar(); | |
renderAppointmentsList(); | |
} | |
}); | |
// Login/Logout | |
loginForm.addEventListener('submit', (e) => { | |
e.preventDefault(); | |
const username = document.getElementById('username').value; | |
const password = document.getElementById('password').value; | |
// Simple validation (in a real app, this would be a backend call) | |
if (username === 'epiliz' && password === 'culoareafericiri') { | |
currentUser = { username: 'admin', name: 'Admin User' }; | |
localStorage.setItem('laserSalonUser', JSON.stringify(currentUser)); | |
loginScreen.classList.add('hidden'); | |
appContainer.classList.remove('hidden'); | |
renderCalendar(); | |
renderAppointmentsList(); | |
} else { | |
alert('Invalid credentials. Try epiliz/culoareafericiri'); | |
} | |
}); | |
logoutBtn.addEventListener('click', () => { | |
currentUser = null; | |
localStorage.removeItem('laserSalonUser'); | |
loginScreen.classList.remove('hidden'); | |
appContainer.classList.add('hidden'); | |
loginForm.reset(); | |
}); | |
// Tab switching | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
// Remove active class from all buttons | |
tabButtons.forEach(btn => { | |
btn.classList.remove('active', 'border-purple-500', 'text-purple-600'); | |
btn.classList.add('border-transparent', 'text-gray-500'); | |
}); | |
// Add active class to clicked button | |
button.classList.add('active', 'border-purple-500', 'text-purple-600'); | |
button.classList.remove('border-transparent', 'text-gray-500'); | |
// Hide all tab contents | |
tabContents.forEach(content => { | |
content.classList.add('hidden'); | |
}); | |
// Show the corresponding tab content | |
const tabId = button.id.replace('-tab', '-content'); | |
document.getElementById(tabId).classList.remove('hidden'); | |
}); | |
}); | |
// Calendar navigation | |
prevMonthBtn.addEventListener('click', () => { | |
currentMonth--; | |
if (currentMonth < 0) { | |
currentMonth = 11; | |
currentYear--; | |
} | |
renderCalendar(); | |
}); | |
nextMonthBtn.addEventListener('click', () => { | |
currentMonth++; | |
if (currentMonth > 11) { | |
currentMonth = 0; | |
currentYear++; | |
} | |
renderCalendar(); | |
}); | |
todayBtn.addEventListener('click', () => { | |
currentDate = new Date(); | |
currentMonth = currentDate.getMonth(); | |
currentYear = currentDate.getFullYear(); | |
renderCalendar(); | |
}); | |
// Close modals | |
closeDayModal.addEventListener('click', () => { | |
dayAppointmentsModal.classList.add('hidden'); | |
}); | |
closeEditModal.addEventListener('click', () => { | |
editAppointmentModal.classList.add('hidden'); | |
}); | |
cancelEdit.addEventListener('click', () => { | |
editAppointmentModal.classList.add('hidden'); | |
}); | |
// Add appointment for specific day | |
addAppointmentForDay.addEventListener('click', () => { | |
const day = modalDayTitle.textContent.match(/(\w+ \d{1,2}, \d{4})/)[0]; | |
const date = new Date(day); | |
const formattedDate = formatDateForInput(date); | |
// Switch to new appointment tab | |
tabButtons.forEach(btn => btn.classList.remove('active', 'border-purple-500', 'text-purple-600')); | |
document.getElementById('new-appointment-tab').classList.add('active', 'border-purple-500', 'text-purple-600'); | |
tabContents.forEach(content => content.classList.add('hidden')); | |
document.getElementById('new-appointment-content').classList.remove('hidden'); | |
// Set the date in the form | |
document.getElementById('appointment-date').value = formattedDate; | |
// Close the modal | |
dayAppointmentsModal.classList.add('hidden'); | |
}); | |
// New appointment form | |
newAppointmentForm.addEventListener('submit', (e) => { | |
e.preventDefault(); | |
const newAppointment = { | |
id: appointments.length > 0 ? Math.max(...appointments.map(a => a.id)) + 1 : 1, | |
clientName: document.getElementById('client-name').value, | |
phone: document.getElementById('client-phone').value, | |
email: document.getElementById('client-email').value, | |
service: document.getElementById('service-package').value, | |
date: document.getElementById('appointment-date').value, | |
time: document.getElementById('appointment-time').value, | |
notes: document.getElementById('appointment-notes').value | |
}; | |
appointments.push(newAppointment); | |
newAppointmentForm.reset(); | |
// Show success message | |
alert('Appointment scheduled successfully!'); | |
// Update views | |
renderCalendar(); | |
renderAppointmentsList(); | |
// Switch to appointments tab | |
tabButtons.forEach(btn => btn.classList.remove('active', 'border-purple-500', 'text-purple-600')); | |
document.getElementById('appointments-tab').classList.add('active', 'border-purple-500', 'text-purple-600'); | |
tabContents.forEach(content => content.classList.add('hidden')); | |
document.getElementById('appointments-content').classList.remove('hidden'); | |
}); | |
// Search appointments | |
searchAppointments.addEventListener('input', () => { | |
renderAppointmentsList(); | |
}); | |
// Export to CSV | |
exportCsvBtn.addEventListener('click', () => { | |
exportToCsv(); | |
}); | |
// Delete appointment | |
deleteAppointmentBtn.addEventListener('click', () => { | |
const appointmentId = parseInt(document.getElementById('edit-appointment-id').value); | |
if (confirm('Are you sure you want to delete this appointment?')) { | |
appointments = appointments.filter(a => a.id !== appointmentId); | |
editAppointmentModal.classList.add('hidden'); | |
renderCalendar(); | |
renderAppointmentsList(); | |
} | |
}); | |
// Edit appointment form | |
editAppointmentForm.addEventListener('submit', (e) => { | |
e.preventDefault(); | |
const appointmentId = parseInt(document.getElementById('edit-appointment-id').value); | |
const appointmentIndex = appointments.findIndex(a => a.id === appointmentId); | |
if (appointmentIndex !== -1) { | |
appointments[appointmentIndex] = { | |
id: appointmentId, | |
clientName: document.getElementById('edit-client-name').value, | |
phone: document.getElementById('edit-client-phone').value, | |
email: document.getElementById('edit-client-email').value, | |
service: document.getElementById('edit-service-package').value, | |
date: document.getElementById('edit-appointment-date').value, | |
time: document.getElementById('edit-appointment-time').value, | |
notes: document.getElementById('edit-appointment-notes').value | |
}; | |
editAppointmentModal.classList.add('hidden'); | |
renderCalendar(); | |
renderAppointmentsList(); | |
} | |
}); | |
// Functions | |
function renderCalendar() { | |
// Update month/year display | |
currentMonthDisplay.textContent = new Date(currentYear, currentMonth).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); | |
// Clear the calendar | |
calendar.innerHTML = ''; | |
// Get first day of month and total days in month | |
const firstDay = new Date(currentYear, currentMonth, 1); | |
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); | |
// Get day of week for first day (0 = Sunday, 6 = Saturday) | |
const firstDayOfWeek = firstDay.getDay(); | |
// Get days from previous month to display | |
const prevMonthDays = new Date(currentYear, currentMonth, 0).getDate(); | |
// Create calendar grid | |
let dayCount = 1; | |
let nextMonthDay = 1; | |
for (let i = 0; i < 35; i++) { // 5 rows x 7 days | |
const dayElement = document.createElement('div'); | |
dayElement.className = 'calendar-day p-2 border border-gray-200 h-24 overflow-y-auto'; | |
if (i < firstDayOfWeek) { | |
// Days from previous month | |
const prevDay = prevMonthDays - (firstDayOfWeek - i - 1); | |
dayElement.innerHTML = `<div class="text-right text-gray-400">${prevDay}</div>`; | |
dayElement.classList.add('text-gray-400'); | |
} else if (dayCount > daysInMonth) { | |
// Days from next month | |
dayElement.innerHTML = `<div class="text-right text-gray-400">${nextMonthDay}</div>`; | |
dayElement.classList.add('text-gray-400'); | |
nextMonthDay++; | |
} else { | |
// Current month days | |
dayElement.innerHTML = `<div class="text-right font-medium">${dayCount}</div>`; | |
// Highlight today | |
const today = new Date(); | |
if (dayCount === today.getDate() && currentMonth === today.getMonth() && currentYear === today.getFullYear()) { | |
dayElement.classList.add('bg-purple-100'); | |
} | |
// Check for appointments on this day | |
const formattedDate = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(dayCount).padStart(2, '0')}`; | |
const dayAppointments = appointments.filter(a => a.date === formattedDate); | |
if (dayAppointments.length > 0) { | |
dayElement.classList.add('has-appointments'); | |
// Add click event to show appointments for the day | |
dayElement.addEventListener('click', () => { | |
showDayAppointments(formattedDate, dayCount); | |
}); | |
// Display appointment count | |
const appointmentCount = document.createElement('div'); | |
appointmentCount.className = 'text-xs mt-1 text-purple-600 font-medium'; | |
appointmentCount.textContent = `${dayAppointments.length} appointment${dayAppointments.length !== 1 ? 's' : ''}`; | |
dayElement.appendChild(appointmentCount); | |
// Display first 2 appointments (if space allows) | |
for (let j = 0; j < Math.min(dayAppointments.length, 2); j++) { | |
const appointment = dayAppointments[j]; | |
const appointmentElement = document.createElement('div'); | |
appointmentElement.className = 'text-xs mt-1 truncate bg-purple-50 px-1 py-0.5 rounded'; | |
appointmentElement.textContent = `${appointment.time} - ${appointment.clientName.split(' ')[0]}`; | |
appointmentElement.title = `${appointment.time} - ${appointment.clientName}: ${appointment.service}`; | |
dayElement.appendChild(appointmentElement); | |
} | |
if (dayAppointments.length > 2) { | |
const moreElement = document.createElement('div'); | |
moreElement.className = 'text-xs mt-1 text-gray-500'; | |
moreElement.textContent = `+${dayAppointments.length - 2} more`; | |
dayElement.appendChild(moreElement); | |
} | |
} else { | |
// Add click event to add new appointment | |
dayElement.addEventListener('click', () => { | |
showDayAppointments(formattedDate, dayCount); | |
}); | |
} | |
dayCount++; | |
} | |
calendar.appendChild(dayElement); | |
} | |
} | |
function showDayAppointments(date, day) { | |
const formattedDate = new Date(date); | |
const dayName = formattedDate.toLocaleDateString('en-US', { weekday: 'long' }); | |
const monthName = formattedDate.toLocaleDateString('en-US', { month: 'long' }); | |
modalDayTitle.textContent = `Appointments for ${dayName}, ${monthName} ${day}, ${currentYear}`; | |
// Filter appointments for this day | |
const dayAppointments = appointments.filter(a => a.date === date); | |
// Clear previous appointments | |
dayAppointmentsList.innerHTML = ''; | |
if (dayAppointments.length === 0) { | |
dayAppointmentsList.innerHTML = '<p class="text-gray-500 text-center py-4">No appointments scheduled for this day.</p>'; | |
} else { | |
// Sort appointments by time | |
dayAppointments.sort((a, b) => a.time.localeCompare(b.time)); | |
// Display each appointment | |
dayAppointments.forEach(appointment => { | |
const appointmentElement = document.createElement('div'); | |
appointmentElement.className = 'appointment-item bg-white border border-gray-200 rounded-md p-3'; | |
appointmentElement.innerHTML = ` | |
<div class="flex justify-between items-start"> | |
<div> | |
<h4 class="font-medium text-gray-800">${appointment.clientName}</h4> | |
<p class="text-sm text-gray-600">${appointment.service} • ${appointment.time}</p> | |
${appointment.notes ? `<p class="text-xs text-gray-500 mt-1">${appointment.notes}</p>` : ''} | |
</div> | |
<div class="flex space-x-2"> | |
<button class="edit-appointment text-purple-600 hover:text-purple-800" data-id="${appointment.id}"> | |
<i class="fas fa-edit"></i> | |
</button> | |
</div> | |
</div> | |
<div class="mt-2 flex items-center text-xs text-gray-500"> | |
<i class="fas fa-phone-alt mr-1"></i> | |
<span class="mr-3">${appointment.phone}</span> | |
${appointment.email ? `<i class="fas fa-envelope mr-1"></i><span>${appointment.email}</span>` : ''} | |
</div> | |
`; | |
dayAppointmentsList.appendChild(appointmentElement); | |
}); | |
// Add event listeners to edit buttons | |
document.querySelectorAll('.edit-appointment').forEach(button => { | |
button.addEventListener('click', (e) => { | |
const appointmentId = parseInt(button.getAttribute('data-id')); | |
showEditAppointmentModal(appointmentId); | |
e.stopPropagation(); | |
}); | |
}); | |
} | |
// Show the modal | |
dayAppointmentsModal.classList.remove('hidden'); | |
} | |
function renderAppointmentsList() { | |
// Clear previous appointments | |
appointmentsTableBody.innerHTML = ''; | |
// Filter appointments based on search | |
const searchTerm = searchAppointments.value.toLowerCase(); | |
let filteredAppointments = [...appointments]; | |
if (searchTerm) { | |
filteredAppointments = appointments.filter(a => | |
a.clientName.toLowerCase().includes(searchTerm) || | |
a.phone.includes(searchTerm) || | |
(a.email && a.email.toLowerCase().includes(searchTerm)) | |
); | |
} | |
// Sort appointments by date and time (soonest 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; | |
}); | |
if (filteredAppointments.length === 0) { | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td colspan="5" class="px-6 py-4 text-center text-gray-500"> | |
No appointments found. Try a different search term or create a new appointment. | |
</td> | |
`; | |
appointmentsTableBody.appendChild(row); | |
return; | |
} | |
// Display each appointment | |
filteredAppointments.forEach(appointment => { | |
const appointmentDate = new Date(`${appointment.date}T${appointment.time}`); | |
const formattedDate = appointmentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
const formattedTime = appointmentDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); | |
const row = document.createElement('tr'); | |
row.className = 'hover:bg-gray-50'; | |
row.innerHTML = ` | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="flex items-center"> | |
<div> | |
<div class="font-medium text-gray-900">${appointment.clientName}</div> | |
</div> | |
</div> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="text-gray-900">${appointment.service}</div> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="text-gray-900">${formattedDate}</div> | |
<div class="text-gray-500">${formattedTime}</div> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="text-gray-900">${appointment.phone}</div> | |
${appointment.email ? `<div class="text-gray-500">${appointment.email}</div>` : ''} | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
<button class="edit-appointment text-purple-600 hover:text-purple-900 mr-3" data-id="${appointment.id}"> | |
<i class="fas fa-edit"></i> Edit | |
</button> | |
<button class="delete-appointment text-red-600 hover:text-red-900" data-id="${appointment.id}"> | |
<i class="fas fa-trash-alt"></i> Delete | |
</button> | |
</td> | |
`; | |
appointmentsTableBody.appendChild(row); | |
}); | |
// Add event listeners to edit and delete buttons | |
document.querySelectorAll('.edit-appointment').forEach(button => { | |
button.addEventListener('click', () => { | |
const appointmentId = parseInt(button.getAttribute('data-id')); | |
showEditAppointmentModal(appointmentId); | |
}); | |
}); | |
document.querySelectorAll('.delete-appointment').forEach(button => { | |
button.addEventListener('click', () => { | |
const appointmentId = parseInt(button.getAttribute('data-id')); | |
if (confirm('Are you sure you want to delete this appointment?')) { | |
appointments = appointments.filter(a => a.id !== appointmentId); | |
renderCalendar(); | |
renderAppointmentsList(); | |
} | |
}); | |
}); | |
} | |
function showEditAppointmentModal(appointmentId) { | |
const appointment = appointments.find(a => a.id === appointmentId); | |
if (!appointment) return; | |
// Fill the form with appointment data | |
document.getElementById('edit-appointment-id').value = appointment.id; | |
document.getElementById('edit-client-name').value = appointment.clientName; | |
document.getElementById('edit-client-phone').value = appointment.phone; | |
document.getElementById('edit-client-email').value = appointment.email || ''; | |
document.getElementById('edit-service-package').value = appointment.service; | |
document.getElementById('edit-appointment-date').value = appointment.date; | |
document.getElementById('edit-appointment-time').value = appointment.time; | |
document.getElementById('edit-appointment-notes').value = appointment.notes || ''; | |
// Show the modal | |
dayAppointmentsModal.classList.add('hidden'); | |
editAppointmentModal.classList.remove('hidden'); | |
} | |
function formatDateForInput(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 exportToCsv() { | |
// Sort appointments by date | |
const sortedAppointments = [...appointments].sort((a, b) => { | |
const dateA = new Date(`${a.date}T${a.time}`); | |
const dateB = new Date(`${b.date}T${b.time}`); | |
return dateA - dateB; | |
}); | |
// CSV header | |
let csv = 'Client Name,Phone,Email,Service,Date,Time,Notes\n'; | |
// Add each appointment | |
sortedAppointments.forEach(appointment => { | |
const row = [ | |
`"${appointment.clientName}"`, | |
`"${appointment.phone}"`, | |
`"${appointment.email || ''}"`, | |
`"${appointment.service}"`, | |
`"${appointment.date}"`, | |
`"${appointment.time}"`, | |
`"${appointment.notes || ''}"` | |
]; | |
csv += row.join(',') + '\n'; | |
}); | |
// Create download link | |
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); | |
const url = URL.createObjectURL(blob); | |
const link = document.createElement('a'); | |
link.setAttribute('href', url); | |
link.setAttribute('download', `laser_salon_appointments_${new Date().toISOString().slice(0, 10)}.csv`); | |
link.style.visibility = 'hidden'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
} | |
</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> |