Booking360 - Master Booking Engine
Booking360
Smart Booking. Simple Operation.
SIFUROBOT SOLUTIONS (003222179-X)
`);
}
/* ============================================================
REPORTS VIEW
============================================================ */
let reportFilter = { start: '', end: '' };
function renderReports() {
if (!reportFilter.start) {
const d = new Date(); d.setDate(d.getDate()-30);
reportFilter.start = d.toISOString().split('T')[0];
reportFilter.end = todayStr();
}
const bkgs = ReportService.bookingsByDateRange(reportFilter.start, reportFilter.end);
const revenue = ReportService.revenueByDateRange(reportFilter.start, reportFilter.end);
const topServices = ReportService.topServices().slice(0,5);
const providerPerf = ReportService.providerPerformance();
const cancelled = bkgs.filter(b => b.status === 'cancelled');
$('contentArea').innerHTML = `
${statCard('Bookings', bkgs.length, 'calendar', 'blue')}
${statCard('Revenue', formatMoney(revenue), 'dollar-sign', 'green')}
${statCard('Cancelled', cancelled.length, 'x-circle', 'red')}
${statCard('Completed', bkgs.filter(b=>b.status==='completed').length, 'check-circle', 'purple')}
${getIndustryPreset().providerLabel} Performance
| ${getIndustryPreset().providerLabel} | Total | Completed | Cancelled | Revenue |
${providerPerf.map(p => `
| ${escapeHtml(p.provider?.name||'-')} |
${p.total} |
${p.completed} |
${p.cancelled} |
${formatMoney(p.revenue)} |
`).join('')}
`;
setTimeout(() => {
State.charts.topServices = new Chart($('topServicesChart'), {
type:'bar',
data:{ labels: topServices.map(t => t.service?.name||'?'), datasets:[{ label:'Bookings', data: topServices.map(t => t.count), backgroundColor:'#2563eb', borderRadius:6 }] },
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{y:{beginAtZero:true}} }
});
// daily chart
const days = {};
bkgs.forEach(b => { days[b.date] = (days[b.date]||0)+1; });
const sortedDays = Object.keys(days).sort();
State.charts.daily = new Chart($('dailyChart'), {
type:'line',
data:{ labels: sortedDays.map(d => formatDate(d)), datasets:[{ label:'Bookings', data: sortedDays.map(d => days[d]), borderColor:'#10b981', backgroundColor:'rgba(16,185,129,.1)', fill:true, tension:.3 }] },
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{y:{beginAtZero:true}} }
});
}, 50);
safeIcons();
}
function exportReportsCSV() {
const bkgs = ReportService.bookingsByDateRange(reportFilter.start, reportFilter.end);
const rows = bkgs.map(b => {
const c = Api.customers.get(b.customer_id);
const s = Api.services.get(b.service_id);
const p = Api.providers.get(b.provider_id);
return { id:b.id, date:b.date, time:b.start_time, customer:c?.name, service:s?.name, provider:p?.name, status:b.status, amount:b.amount, payment:b.payment_status };
});
exportCSV('report-bookings.csv', rows);
}
/* ============================================================
USERS VIEW
============================================================ */
let userFilters = { search:'', role:'', status:'' };
function renderUsers() {
let users = Api.users.list();
if (State.user.role === 'business_admin') users = users.filter(u => u.business_id === getCurrentBusinessId());
if (userFilters.search) { const q = userFilters.search.toLowerCase(); users = users.filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)); }
if (userFilters.role) users = users.filter(u => u.role === userFilters.role);
if (userFilters.status) users = users.filter(u => u.status === userFilters.status);
$('contentArea').innerHTML = `
| Name | Email | Phone | Role | Status | Actions |
${users.map(u => `
| ${escapeHtml(u.name)} |
${escapeHtml(u.email)} |
${escapeHtml(u.phone||'-')} |
${roleLabel(u.role)} |
${statusActiveBadge(u.status)} |
|
`).join('')}
`;
safeIcons();
}
function openUserForm(id=null) {
const u = id ? Api.users.get(id) : null;
openModal(`
`);
}
function submitUserForm(id) {
const f = $('userForm');
if (!f.name.value || !f.email.value || !/\S+@\S+\.\S+/.test(f.email.value)) { toast('Valid name & email required','error'); return; }
const data = { name:f.name.value, email:f.email.value, phone:f.phone.value, role:f.role.value, status:f.status.value, business_id: getCurrentBusinessId() };
if (f.password.value) data.password = f.password.value;
if (id) Api.users.update(id, data); else Api.users.create({...data, password: data.password||'password'});
toast(id?'Updated':'Added','success'); closeModal(); renderPage();
}
function toggleUserStatus(id) {
const u = Api.users.get(id);
Api.users.update(id, { status: u.status === 'active' ? 'inactive' : 'active' });
toast('Status updated','success'); renderPage();
}
/* ============================================================
AUDIT LOG
============================================================ */
function renderAuditLog() {
const logs = Store.get('app_audit_logs').slice(0,100);
$('contentArea').innerHTML = `
| Time | Action | Module | Description | User |
${logs.map(l => {
const u = Api.users.get(l.user_id);
return `
| ${formatDateTime(l.created_at)} |
${l.action} |
${escapeHtml(l.module)} |
${escapeHtml(l.description)} |
${escapeHtml(u?.name||'System')} |
`;
}).join('')}
`;
}
/* ============================================================
SETTINGS VIEW
============================================================ */
function renderSettings() {
const biz = getCurrentBusiness();
$('contentArea').innerHTML = `
System
${APP.name} ${APP.version}
${APP.company}
${APP.tagline}
Resets all data to factory seed.
`;
safeIcons();
}
function saveBizInfo() {
const f = $('bizForm');
Api.businesses.update(getCurrentBusinessId(), {
name: f.name.value, industry: f.industry.value, phone: f.phone.value,
email: f.email.value, address: f.address.value, logo: f.logo.value,
theme_color: f.theme_color.value
});
toast('Business info saved','success');
renderSidebar();
renderPage();
}
function saveHours() {
const f = $('hoursForm');
const off_days = [];
for (let i=0; i<7; i++) if (f[`off${i}`].checked) off_days.push(i);
Api.businesses.update(getCurrentBusinessId(), {
opening_hours: { start: f.open.value, end: f.close.value },
break_time: f.break_start.value ? { start: f.break_start.value, end: f.break_end.value } : null,
slot_interval: parseInt(f.interval.value),
off_days,
cancellation_hours: parseInt(f.cancel_hours.value)
});
toast('Hours saved','success');
}
function saveNotif() {
const f = $('notifForm');
Api.businesses.update(getCurrentBusinessId(), {
reminder_enabled: f.reminder.checked,
email_notify: f.email_notify.checked,
whatsapp_api: f.whatsapp.value
});
toast('Notification settings saved','success');
}
function resetDemoData() {
confirmModal('Reset Demo Data', 'This will erase ALL current data and reload factory seed. Continue?', () => {
Auth.logout();
Store.reset();
toast('Demo data reset. Logging in again...','success');
setTimeout(() => location.reload(), 800);
});
}
/* ============================================================
PROFILE VIEW (Customer)
============================================================ */
function renderProfile() {
const u = State.user;
const c = Api.customers.list().find(c => c.user_id === u.id);
$('contentArea').innerHTML = `
`;
}
function saveProfile() {
const f = $('profileForm');
const data = { name: f.name.value, phone: f.phone.value };
if (f.password.value) data.password = f.password.value;
Api.users.update(State.user.id, data);
const c = Api.customers.list().find(c => c.user_id === State.user.id);
if (c) Api.customers.update(c.id, { name: f.name.value, phone: f.phone.value, dob: f.dob?.value || c.dob, address: f.address?.value || c.address });
State.user = Api.users.get(State.user.id);
toast('Profile updated','success');
renderSidebar();
}
/* ============================================================
NOTIFICATIONS DROPDOWN
============================================================ */
function openNotifications() {
const notifs = Api.notifications.list(State.user.id).slice(0,10);
openModal(`
`);
}
/* ============================================================
AUTH HANDLERS
============================================================ */
function switchAuth(view) {
['loginForm','registerForm','forgotForm'].forEach(f => $(f).classList.add('hide'));
$(view+'Form').classList.remove('hide');
}
function fillLogin(email) { $('loginEmail').value = email; $('loginPassword').value = 'password'; }
function handleLogin() {
const email = $('loginEmail').value.trim();
const password = $('loginPassword').value;
if (!email || !password) { toast('Email and password required','error'); return; }
const user = Auth.login(email, password);
if (!user) { toast('Invalid credentials','error'); return; }
State.user = user;
toast(`Welcome, ${user.name}!`,'success');
enterApp();
}
function handleRegister() {
const name = $('regName').value.trim();
const email = $('regEmail').value.trim();
const phone = $('regPhone').value.trim();
const password = $('regPassword').value;
if (!name || !email || !phone || !password) { toast('All fields required','error'); return; }
if (!/\S+@\S+\.\S+/.test(email)) { toast('Invalid email','error'); return; }
const result = Auth.register({ name, email, phone, password });
if (result.error) { toast(result.error,'error'); return; }
toast('Registered! Please login.','success');
$('loginEmail').value = email; $('loginPassword').value = password;
switchAuth('login');
}
function handleForgot() {
toast('Reset link sent (placeholder). Use password "password" to login.','info');
switchAuth('login');
}
function handleLogout() {
confirmModal('Sign Out', 'Are you sure you want to sign out?', () => {
Auth.logout();
$('appView').classList.add('hide');
$('authView').classList.remove('hide');
State.currentView = 'dashboard';
});
}
function enterApp() {
$('authView').classList.add('hide');
$('appView').classList.remove('hide');
State.currentView = 'dashboard';
renderSidebar();
renderPage();
}
/* ============================================================
INIT APP
============================================================ */
function init() {
seedData();
const user = Auth.current();
if (user) {
State.user = user;
enterApp();
} else {
$('authView').classList.remove('hide');
$('appView').classList.add('hide');
}
safeIcons();
}
window.addEventListener('load', init);