05/07 Phase 3: initial codes

This commit is contained in:
2026-05-07 12:17:19 -04:00
parent 6e5518c103
commit ce462c57b2
107 changed files with 6365 additions and 208 deletions
+75
View File
@@ -0,0 +1,75 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Appointment{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Appointment</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Start Date & Time <span class="text-danger">*</span></label>
<input type="datetime-local" class="form-control" name="start_time" required
value="{{ appointment.start_time.strftime('%Y-%m-%dT%H:%M') if appointment else '' }}">
</div>
<div class="col-md-6">
<label class="form-label">Service</label>
<select class="form-select" name="service_id">
<option value="">— None —</option>
{% for s in services %}
<option value="{{ s.id }}"
{{ 'selected' if appointment and appointment.service_id == s.id }}>
{{ s.name }} ({{ s.duration_min }} min)
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Customer</label>
<select class="form-select" name="customer_id">
<option value="">— Walk-in —</option>
{% for c in customers %}
<option value="{{ c.id }}"
{{ 'selected' if appointment and appointment.customer_id == c.id }}>
{{ c.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Staff</label>
<select class="form-select" name="staff_id">
<option value="">— Any —</option>
{% for s in staff_list %}
<option value="{{ s.id }}"
{{ 'selected' if appointment and appointment.staff_id == s.id }}>
{{ s.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_walk_in" value="1"
id="is_walk_in" {{ 'checked' if appointment and appointment.is_walk_in }}>
<label class="form-check-label" for="is_walk_in">Walk-in (no appointment)</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Notes</label>
<textarea class="form-control" name="notes" rows="2">{{ appointment.notes if appointment else '' }}</textarea>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('appointments.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+56
View File
@@ -0,0 +1,56 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Appointments{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="fw-bold mb-0"><i class="bi bi-calendar3 me-2"></i>Appointments</h4>
<div class="d-flex align-items-center gap-2">
<a href="{{ url_for('appointments.index', date=(view_date - timedelta(days=1)).isoformat()) }}"
class="btn btn-outline-secondary btn-sm"></a>
<input type="date" class="form-control form-control-sm" value="{{ view_date.isoformat() }}"
onchange="window.location='{{ url_for('appointments.index') }}?date='+this.value"
style="width:150px;">
<a href="{{ url_for('appointments.index', date=(view_date + timedelta(days=1)).isoformat()) }}"
class="btn btn-outline-secondary btn-sm"></a>
<a href="{{ url_for('appointments.create') }}" class="btn btn-primary btn-sm ms-2">+ New</a>
</div>
</div>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Time</th><th>Customer</th><th>Service</th><th>Staff</th><th>Status</th><th>Type</th><th></th></tr>
</thead>
<tbody>
{% for appt in appointments %}
<tr>
<td class="fw-semibold text-nowrap">{{ appt.start_time.strftime('%I:%M %p') }}</td>
<td>{{ appt.customer.name if appt.customer else '<em class="text-muted">Walk-in</em>' | safe }}</td>
<td class="small">{{ appt.service.name if appt.service else '—' }}</td>
<td class="small">{{ appt.staff.name if appt.staff else '—' }}</td>
<td>
<span class="badge bg-{{ {'pending':'warning','confirmed':'primary','in_progress':'info','completed':'success','cancelled':'danger','no_show':'dark'}.get(appt.status,'secondary') }}">
{{ appt.status }}
</span>
</td>
<td class="small">{{ 'Walk-in' if appt.is_walk_in else 'Booked' }}</td>
<td>
<a href="{{ url_for('appointments.view', appt_id=appt.id) }}" class="btn btn-outline-secondary btn-sm">View</a>
{% if appt.status in ['pending','confirmed','in_progress'] %}
<a href="{{ url_for('pos.checkout') }}?appointment_id={{ appt.id }}" class="btn btn-success btn-sm">Checkout</a>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">No appointments for this day.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Import timedelta not available in Jinja — use JS for date nav
</script>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Appointment #{{ appointment.id }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">Appointment #{{ appointment.id }}</h4>
<div class="d-flex gap-2">
<a href="{{ url_for('appointments.edit', appt_id=appointment.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
{% if appointment.status in ['pending','confirmed','in_progress'] %}
<a href="{{ url_for('pos.checkout') }}?appointment_id={{ appointment.id }}" class="btn btn-success btn-sm">
<i class="bi bi-cash-register me-1"></i>Checkout
</a>
{% endif %}
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Customer</dt><dd class="col-sm-8">{{ appointment.customer.name if appointment.customer else 'Walk-in' }}</dd>
<dt class="col-sm-4">Service</dt><dd class="col-sm-8">{{ appointment.service.name if appointment.service else '—' }}</dd>
<dt class="col-sm-4">Staff</dt><dd class="col-sm-8">{{ appointment.staff.name if appointment.staff else '—' }}</dd>
<dt class="col-sm-4">Start</dt><dd class="col-sm-8">{{ appointment.start_time.strftime('%Y-%m-%d %I:%M %p') }}</dd>
<dt class="col-sm-4">End</dt><dd class="col-sm-8">{{ appointment.end_time.strftime('%I:%M %p') if appointment.end_time else '—' }}</dd>
<dt class="col-sm-4">Type</dt><dd class="col-sm-8">{{ 'Walk-in' if appointment.is_walk_in else 'Booked' }}</dd>
<dt class="col-sm-4">Source</dt><dd class="col-sm-8">{{ appointment.rebook_source or 'manual' }}</dd>
{% if appointment.notes %}
<dt class="col-sm-4">Notes</dt><dd class="col-sm-8">{{ appointment.notes }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Update Status</div>
<div class="card-body">
<form method="POST" action="{{ url_for('appointments.set_status', appt_id=appointment.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<select class="form-select" name="status">
{% for s in ['pending','confirmed','in_progress','completed','cancelled','no_show'] %}
<option value="{{ s }}" {{ 'selected' if appointment.status == s }}>{{ s.replace('_',' ').title() }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3" id="cancel-reason-field">
<label class="form-label small">Cancellation reason</label>
<input type="text" class="form-control form-control-sm" name="reason"
value="{{ appointment.cancellation_reason or '' }}">
</div>
<button type="submit" class="btn btn-primary btn-sm">Update Status</button>
</form>
</div>
</div>
</div>
</div>
<a href="{{ url_for('appointments.index', date=appointment.start_time.date().isoformat()) }}"
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Back to Calendar
</a>
{% endblock %}
+79
View File
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Book an Appointment — {{ tenant.name }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container py-5" style="max-width:540px;">
{% if confirmed %}
<div class="card shadow text-center p-5">
<div class="display-1 mb-3"></div>
<h3 class="fw-bold">Booking Requested!</h3>
<p class="text-muted">We'll confirm your appointment shortly. Check your email for details.</p>
<a href="/book/{{ tenant.slug }}" class="btn btn-outline-primary mt-3">Book Another</a>
</div>
{% else %}
<div class="card shadow p-4">
<h3 class="fw-bold mb-1">{{ tenant.name }}</h3>
<p class="text-muted mb-4">Book an appointment online</p>
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Your Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" required autofocus>
</div>
<div class="col">
<label class="form-label">Phone <span class="text-danger">*</span></label>
<input type="tel" class="form-control" name="phone" required inputmode="numeric">
</div>
</div>
<div class="mb-3">
<label class="form-label">Email <small class="text-muted">(for confirmation)</small></label>
<input type="email" class="form-control" name="email">
</div>
<div class="mb-3">
<label class="form-label">Service <span class="text-danger">*</span></label>
<select class="form-select" name="service_id" required>
<option value="">— Select a service —</option>
{% for s in services %}
<option value="{{ s.id }}">{{ s.name }} — ${{ "%.2f"|format(s.price) }} ({{ s.duration_min }} min)</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Preferred Staff <small class="text-muted">(optional)</small></label>
<select class="form-select" name="staff_id">
<option value="">— No preference —</option>
{% for s in staff_list %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="preferred_date" required>
</div>
<div class="col">
<label class="form-label">Time <span class="text-danger">*</span></label>
<input type="time" class="form-control" name="preferred_time" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" name="notes" rows="2"></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Request Appointment</button>
</div>
</form>
</div>
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Not Found</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body class="bg-light d-flex align-items-center justify-content-center" style="min-height:100vh;">
<div class="text-center">
<h2 class="fw-bold">Booking Page Not Found</h2>
<p class="text-muted">This salon's booking page doesn't exist or is no longer available.</p>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Booking Unavailable</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body class="bg-light d-flex align-items-center justify-content-center" style="min-height:100vh;">
<div class="text-center">
<h2 class="fw-bold">Online Booking Unavailable</h2>
<p class="text-muted">Online booking is not currently available for this salon. Please call to book.</p>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Check-In Not Found</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body class="bg-light d-flex align-items-center justify-content-center" style="min-height:100vh;">
<div class="text-center">
<h2 class="fw-bold">Check-In Unavailable</h2>
<p class="text-muted">This check-in kiosk is not available. Please see a staff member.</p>
</div>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Customer{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Customer</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name"
value="{{ customer.name if customer else '' }}" required autofocus>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Phone</label>
<input type="tel" class="form-control" name="phone"
value="{{ customer.phone if customer else '' }}">
</div>
<div class="col">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email"
value="{{ customer.email if customer else '' }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">Date of Birth</label>
<input type="date" class="form-control" name="date_of_birth"
value="{{ customer.date_of_birth.isoformat() if customer and customer.date_of_birth else '' }}">
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" name="notes" rows="3">{{ customer.notes if customer else '' }}</textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('customers.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+41
View File
@@ -0,0 +1,41 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Customers{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-people me-2"></i>Customers</h4>
<a href="{{ url_for('customers.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>New Customer
</a>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<form method="GET" class="d-flex gap-2">
<input type="search" class="form-control form-control-sm" name="q"
value="{{ q }}" placeholder="Search by name, phone, or email…" autofocus>
<button class="btn btn-outline-secondary btn-sm">Search</button>
{% if q %}<a href="{{ url_for('customers.index') }}" class="btn btn-outline-danger btn-sm">Clear</a>{% endif %}
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Name</th><th>Phone</th><th>Email</th><th>Loyalty</th><th>No-Shows</th><th></th></tr></thead>
<tbody>
{% for c in customers %}
<tr>
<td><a href="{{ url_for('customers.view', customer_id=c.id) }}" class="text-decoration-none fw-semibold">{{ c.name }}</a></td>
<td class="text-muted small">{{ c.phone or '—' }}</td>
<td class="text-muted small">{{ c.email or '—' }}</td>
<td><span class="badge bg-warning text-dark">{{ c.loyalty_points }} pts</span></td>
<td>{% if c.no_show_count > 0 %}<span class="badge bg-danger">{{ c.no_show_count }}</span>{% else %}0{% endif %}</td>
<td><a href="{{ url_for('customers.edit', customer_id=c.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a></td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-4">No customers found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+58
View File
@@ -0,0 +1,58 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ customer.name }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">{{ customer.name }}</h4>
<div class="d-flex gap-2">
<a href="{{ url_for('customers.edit', customer_id=customer.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<a href="{{ url_for('appointments.create') }}?customer_id={{ customer.id }}" class="btn btn-primary btn-sm">New Appointment</a>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body">
<p class="mb-1"><i class="bi bi-telephone me-2 text-muted"></i>{{ customer.phone or '—' }}</p>
<p class="mb-1"><i class="bi bi-envelope me-2 text-muted"></i>{{ customer.email or '—' }}</p>
{% if customer.date_of_birth %}
<p class="mb-1"><i class="bi bi-cake me-2 text-muted"></i>{{ customer.date_of_birth.strftime('%B %d') }}</p>
{% endif %}
<p class="mb-1"><i class="bi bi-star me-2 text-warning"></i>{{ customer.loyalty_points }} loyalty points</p>
{% if customer.no_show_count > 0 %}
<p class="mb-0"><i class="bi bi-x-circle me-2 text-danger"></i>{{ customer.no_show_count }} no-show(s)</p>
{% endif %}
{% if customer.notes %}
<hr><p class="text-muted small mb-0">{{ customer.notes }}</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">Recent Appointments</div>
{% if appointments %}
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Date</th><th>Service</th><th>Staff</th><th>Status</th></tr></thead>
<tbody>
{% for a in appointments %}
<tr>
<td class="small">{{ a.start_time.strftime('%Y-%m-%d %I:%M %p') }}</td>
<td class="small">{{ a.service.name if a.service else '—' }}</td>
<td class="small">{{ a.staff.name if a.staff else '—' }}</td>
<td><span class="badge bg-{{ {'completed':'success','cancelled':'danger','no_show':'warning'}.get(a.status,'secondary') }} small">{{ a.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card-body text-muted small">No appointments yet.</div>
{% endif %}
</div>
</div>
</div>
<a href="{{ url_for('customers.index') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
{% endblock %}
+62 -6
View File
@@ -1,10 +1,66 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h4 class="mb-4">Dashboard
{% if location %}<small class="text-muted fs-6">— {{ location.name }}</small>{% endif %}
</h4>
<div class="alert alert-info">
Phase 3 KPI widgets will appear here (daily revenue, appointments, staff on-shift, low-stock alerts).
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">
<i class="bi bi-speedometer2 me-2"></i>Dashboard
{% if location %}<small class="text-muted fs-6 ms-2">— {{ location.name }}</small>{% endif %}
</h4>
<span class="text-muted small">{{ today.strftime('%A, %B %d, %Y') }}</span>
</div>
{% endblock %}
<!-- KPI cards -->
<div class="row g-3 mb-4">
{% set kpi_items = [
("Today's Revenue", "$" ~ "%.2f"|format(kpis.today_revenue), "graph-up-arrow", "success"),
("Appointments", kpis.total_appointments, "calendar3", "primary"),
("Completed", kpis.completed_appointments, "check-circle", "success"),
("Pending", kpis.pending_appointments, "hourglass-split", "warning"),
("Staff on Shift", kpis.staff_on_shift, "person-badge", "info"),
("Check-in Queue", kpis.queue_count, "door-open", "danger" if kpis.queue_count > 0 else "secondary"),
] %}
{% for label, value, icon, color in kpi_items %}
<div class="col-sm-6 col-lg-4 col-xl-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="rounded-circle bg-{{ color }} bg-opacity-10 p-2">
<i class="bi bi-{{ icon }} fs-5 text-{{ color }}"></i>
</div>
<div>
<div class="fw-bold fs-5">{{ value }}</div>
<div class="text-muted" style="font-size:.75rem;">{{ label }}</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Upcoming appointments -->
<div class="card shadow-sm">
<div class="card-header fw-semibold d-flex justify-content-between align-items-center">
<span><i class="bi bi-calendar-check me-2"></i>Upcoming Today</span>
<a href="{{ url_for('appointments.index') }}" class="btn btn-outline-primary btn-sm">Full Calendar</a>
</div>
{% if upcoming_appointments %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Time</th><th>Customer</th><th>Service</th><th>Staff</th><th>Status</th></tr></thead>
<tbody>
{% for appt in upcoming_appointments %}
<tr>
<td class="fw-semibold">{{ appt.start_time.strftime('%I:%M %p') }}</td>
<td>{{ appt.customer.name if appt.customer else '—' }}</td>
<td>{{ appt.service.name if appt.service else '—' }}</td>
<td>{{ appt.staff.name if appt.staff else '—' }}</td>
<td><span class="badge bg-{{ {'pending':'warning','confirmed':'primary','in_progress':'info','completed':'success'}.get(appt.status,'secondary') }}">{{ appt.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card-body text-muted small">No upcoming appointments for today.</div>
{% endif %}
</div>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Issue Gift Card{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Issue New Gift Card</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Value ($) <span class="text-danger">*</span></label>
<input type="number" step="0.01" min="1" class="form-control"
name="value" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Customer <small class="text-muted">(optional)</small></label>
<select class="form-select" name="customer_id">
<option value="">— None —</option>
{% for c in customers %}
<option value="{{ c.id }}">{{ c.name }}{% if c.phone %} — {{ c.phone }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Expiry Date <small class="text-muted">(optional)</small></label>
<input type="datetime-local" class="form-control" name="expires_at">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Issue Card</button>
<a href="{{ url_for('gift_cards.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+49
View File
@@ -0,0 +1,49 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Gift Cards{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-gift me-2"></i>Gift Cards</h4>
<div class="d-flex gap-2">
<a href="{{ url_for('gift_cards.lookup') }}" class="btn btn-outline-secondary btn-sm">Lookup Code</a>
<a href="{{ url_for('gift_cards.issue') }}" class="btn btn-primary btn-sm">+ Issue Gift Card</a>
</div>
</div>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Code</th><th>Original</th><th>Balance</th><th>Customer</th><th>Expires</th><th>Status</th><th></th></tr>
</thead>
<tbody>
{% for card in cards %}
<tr class="{{ '' if card.is_active else 'text-muted' }}">
<td class="font-monospace fw-semibold">{{ card.code }}</td>
<td>${{ "%.2f"|format(card.original_value) }}</td>
<td class="fw-semibold {% if card.remaining_balance <= 0 %}text-danger{% endif %}">
${{ "%.2f"|format(card.remaining_balance) }}
</td>
<td class="small">{{ card.customer.name if card.issued_to_customer_id and card.customer else '—' }}</td>
<td class="small text-muted">{{ card.expires_at.strftime('%Y-%m-%d') if card.expires_at else 'No expiry' }}</td>
<td>
<span class="badge {{ 'bg-success' if card.is_active else 'bg-secondary' }}">
{{ 'Active' if card.is_active else 'Inactive' }}
</span>
</td>
<td>
{% if card.is_active %}
<form method="POST" action="{{ url_for('gift_cards.deactivate', card_id=card.id) }}"
class="d-inline" onsubmit="return confirm('Deactivate this gift card?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-danger btn-sm">Deactivate</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">No gift cards issued yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+40
View File
@@ -0,0 +1,40 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Gift Card Lookup{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Gift Card Lookup</div>
<div class="card-body">
<form method="GET" class="mb-3">
<div class="input-group">
<input type="text" class="form-control font-monospace" name="code"
value="{{ code }}" placeholder="XXXX-XXXX-XXXX" autofocus>
<button class="btn btn-primary">Look Up</button>
</div>
</form>
{% if code and card %}
<div class="alert {{ 'alert-success' if card.is_active else 'alert-warning' }}">
<dl class="row mb-0">
<dt class="col-sm-5">Code</dt><dd class="col-sm-7 font-monospace">{{ card.code }}</dd>
<dt class="col-sm-5">Original Value</dt><dd class="col-sm-7">${{ "%.2f"|format(card.original_value) }}</dd>
<dt class="col-sm-5">Remaining Balance</dt>
<dd class="col-sm-7 fw-bold {% if card.remaining_balance <= 0 %}text-danger{% endif %}">
${{ "%.2f"|format(card.remaining_balance) }}
</dd>
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">{{ 'Active' if card.is_active else 'Inactive / Depleted' }}</dd>
{% if card.expires_at %}
<dt class="col-sm-5">Expires</dt><dd class="col-sm-7">{{ card.expires_at.strftime('%Y-%m-%d') }}</dd>
{% endif %}
</dl>
</div>
{% elif code %}
<div class="alert alert-danger">No gift card found with code <strong>{{ code }}</strong>.</div>
{% endif %}
<a href="{{ url_for('gift_cards.index') }}" class="btn btn-outline-secondary btn-sm">Back</a>
</div>
</div>
</div>
</div>
{% endblock %}
+10 -10
View File
@@ -37,7 +37,7 @@
{% for loc in locations %}
<li>
<a class="dropdown-item {% if g.location and g.location.id == loc.id %}active{% endif %}"
href="{{ '#' }}">
href="{{ url_for('locations.switch', location_id=loc.id) }}">
{{ loc.name }}
{% if loc.is_primary %}<span class="badge bg-secondary ms-1">Primary</span>{% endif %}
</a>
@@ -86,31 +86,31 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'appointments' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('appointments.index') }}">
<i class="bi bi-calendar3 me-2"></i>Appointments
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'pos' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('pos.checkout') }}">
<i class="bi bi-cash-register me-2"></i>POS / Checkout
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'customers' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('customers.index') }}">
<i class="bi bi-people me-2"></i>Customers
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'staff' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('staff.index') }}">
<i class="bi bi-person-badge me-2"></i>Staff
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'services' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('services.index') }}">
<i class="bi bi-card-list me-2"></i>Services & Products
</a>
</li>
@@ -122,13 +122,13 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'gift_cards' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('gift_cards.index') }}">
<i class="bi bi-gift me-2"></i>Gift Cards
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reconciliation' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('reconciliation.index') }}">
<i class="bi bi-calculator me-2"></i>Reconciliation
</a>
</li>
@@ -147,13 +147,13 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'locations' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('locations.index') }}">
<i class="bi bi-geo-alt me-2"></i>Locations
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'settings' in request.endpoint %}active fw-bold{% endif %}"
href="{{ '#' }}">
href="{{ url_for('settings.index') }}">
<i class="bi bi-gear me-2"></i>Settings
</a>
</li>
+62
View File
@@ -0,0 +1,62 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Location{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Location</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name"
value="{{ location.name if location else '' }}" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address"
value="{{ location.address if location else '' }}">
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Phone</label>
<input type="tel" class="form-control" name="phone"
value="{{ location.phone if location else '' }}">
</div>
<div class="col">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email"
value="{{ location.email if location else '' }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">Timezone</label>
<input type="text" class="form-control" name="timezone"
value="{{ location.timezone if location else 'America/New_York' }}">
</div>
{% if mode == 'edit' %}
<div class="mb-3 d-flex gap-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="is_active" {{ 'checked' if location and location.is_active }}>
<label class="form-check-label" for="is_active">Active</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_primary" value="1"
id="is_primary" {{ 'checked' if location and location.is_primary }}>
<label class="form-check-label" for="is_primary">Set as Primary</label>
</div>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('locations.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Locations{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-geo-alt me-2"></i>Locations</h4>
<a href="{{ url_for('locations.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>New Location
</a>
</div>
<div class="row g-3">
{% for loc in locations %}
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm h-100 {{ '' if loc.is_active else 'opacity-50' }}">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>{{ loc.name }}</strong>
<div>
{% if loc.is_primary %}<span class="badge bg-primary me-1">Primary</span>{% endif %}
<span class="badge {{ 'bg-success' if loc.is_active else 'bg-secondary' }}">{{ 'Active' if loc.is_active else 'Inactive' }}</span>
</div>
</div>
<div class="card-body small text-muted">
{% if loc.address %}<p class="mb-1"><i class="bi bi-map me-1"></i>{{ loc.address }}</p>{% endif %}
{% if loc.phone %}<p class="mb-1"><i class="bi bi-telephone me-1"></i>{{ loc.phone }}</p>{% endif %}
{% if loc.email %}<p class="mb-1"><i class="bi bi-envelope me-1"></i>{{ loc.email }}</p>{% endif %}
<p class="mb-0"><i class="bi bi-clock me-1"></i>{{ loc.timezone }}</p>
</div>
<div class="card-footer d-flex gap-2">
<a href="{{ url_for('locations.edit', location_id=loc.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
{% if not loc.is_primary %}
<form method="POST" action="{{ url_for('locations.set_primary', location_id=loc.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-primary btn-sm">Set Primary</button>
</form>
{% endif %}
<a href="{{ url_for('locations.switch', location_id=loc.id) }}" class="btn btn-outline-success btn-sm ms-auto">Switch</a>
</div>
</div>
</div>
{% else %}
<div class="col"><p class="text-muted">No locations found.</p></div>
{% endfor %}
</div>
{% endblock %}
+115
View File
@@ -0,0 +1,115 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}POS Checkout{% endblock %}
{% block content %}
<h4 class="fw-bold mb-4"><i class="bi bi-cash-register me-2"></i>Checkout</h4>
<form method="POST" action="{{ url_for('pos.submit') }}" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if appointment %}
<input type="hidden" name="appointment_id" value="{{ appointment.id }}">
{% endif %}
<div class="row g-3">
<!-- Left: items -->
<div class="col-lg-7">
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">Services</div>
<div class="card-body">
{% for s in services %}
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" name="service_ids"
value="{{ s.id }}" id="svc_{{ s.id }}"
{{ 'checked' if appointment and appointment.service_id == s.id }}>
<label class="form-check-label" for="svc_{{ s.id }}">
{{ s.name }} <span class="text-muted small">({{ s.duration_min }}min)</span>
<span class="fw-semibold">${{ "%.2f"|format(s.price) }}</span>
</label>
</div>
{% endfor %}
</div>
</div>
{% if services %}
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">Products</div>
<div class="card-body">
{% for p in products %}
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" name="product_ids"
value="{{ p.id }}" id="prod_{{ p.id }}">
<label class="form-check-label" for="prod_{{ p.id }}">
{{ p.name }} <span class="fw-semibold">${{ "%.2f"|format(p.sale_price) }}</span>
</label>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Right: payment details -->
<div class="col-lg-5">
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">Payment Details</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Customer</label>
<select class="form-select" name="customer_id">
<option value="">— Walk-in / No Customer —</option>
{% for c in customers %}
<option value="{{ c.id }}"
{{ 'selected' if appointment and appointment.customer_id == c.id }}>
{{ c.name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Staff</label>
<select class="form-select" name="staff_id">
<option value="">— Not assigned —</option>
{% for s in staff_list %}
<option value="{{ s.id }}"
{{ 'selected' if appointment and appointment.staff_id == s.id }}>
{{ s.name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Payment Method</label>
<select class="form-select" name="payment_method">
{% for m in payment_methods %}
<option value="{{ m }}">{{ m.title() }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Payment Reference <small class="text-muted">(optional)</small></label>
<input type="text" class="form-control" name="payment_reference"
placeholder="Transaction ID, note…">
</div>
<div class="mb-3">
<label class="form-label">Tip ($)</label>
<input type="number" step="0.01" class="form-control" name="tip_amount"
value="0" min="0">
</div>
<div class="mb-3">
<label class="form-label">Gift Card Code</label>
<input type="text" class="form-control font-monospace" name="gift_card_code"
placeholder="XXXX-XXXX-XXXX">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="rebook" value="1" id="rebook">
<label class="form-check-label" for="rebook">Schedule next visit after checkout</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg">
<i class="bi bi-check-circle me-2"></i>Complete Checkout
</button>
</div>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Schedule Next Visit{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold"><i class="bi bi-calendar-plus me-2"></i>Schedule Next Visit</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Date & Time <span class="text-danger">*</span></label>
<input type="datetime-local" class="form-control" name="start_time" required>
</div>
<div class="mb-3">
<label class="form-label">Service</label>
<select class="form-select" name="service_id">
<option value="">— Same as today —</option>
{% for s in services %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Staff</label>
<select class="form-select" name="staff_id">
<option value="">— Any —</option>
{% for s in staff_list %}
<option value="{{ s.id }}" {{ 'selected' if transaction.staff_id == s.id }}>{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Schedule</button>
<a href="{{ url_for('pos.receipt', transaction_id=transaction.id) }}" class="btn btn-outline-secondary">Skip</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Receipt #{{ transaction.id }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body p-4">
<div class="text-center mb-4">
<h5 class="fw-bold">{{ tenant.name }}</h5>
{% if location %}<p class="text-muted small mb-0">{{ location.name }}</p>{% endif %}
<p class="text-muted small">{{ transaction.created_at.strftime('%B %d, %Y %I:%M %p') }}</p>
<p class="text-muted small">Receipt #{{ transaction.id }}</p>
{% if transaction.voided_at %}
<div class="alert alert-danger py-1 mt-2">VOIDED — {{ transaction.void_reason }}</div>
{% endif %}
</div>
<table class="table table-sm mb-3">
<tbody>
{% for item in items %}
<tr>
<td>
{% if item.service %}{{ item.service.name }}
{% elif item.product %}{{ item.product.name }}
{% endif %}
{% if item.discount_percent > 0 %}
<span class="badge bg-success ms-1">{{ item.discount_percent }}% off</span>
{% endif %}
</td>
<td class="text-end">
{% if item.discount_percent > 0 %}
<del class="text-muted small">${{ "%.2f"|format(item.original_price) }}</del>
{% endif %}
${{ "%.2f"|format(item.unit_price) }}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="fw-semibold">
{% if transaction.discount > 0 %}
<tr class="text-success"><td>Savings</td><td class="text-end">-${{ "%.2f"|format(transaction.discount) }}</td></tr>
{% endif %}
{% if transaction.tip_amount > 0 %}
<tr><td>Tip</td><td class="text-end">${{ "%.2f"|format(transaction.tip_amount) }}</td></tr>
{% endif %}
{% if transaction.gift_card_amount > 0 %}
<tr class="text-info"><td>Gift Card</td><td class="text-end">-${{ "%.2f"|format(transaction.gift_card_amount) }}</td></tr>
{% endif %}
<tr class="table-dark fs-5"><td>Total</td><td class="text-end">${{ "%.2f"|format(transaction.total) }}</td></tr>
<tr><td>Payment</td><td class="text-end">{{ transaction.payment_method.title() }}</td></tr>
</tfoot>
</table>
<div class="d-flex gap-2 justify-content-center flex-wrap">
<a href="{{ url_for('pos.checkout') }}" class="btn btn-primary">New Transaction</a>
<a href="{{ url_for('pos.rebook', transaction_id=transaction.id) }}" class="btn btn-outline-primary">Rebook</a>
{% if not transaction.voided_at %}
<a href="{{ url_for('pos.void', transaction_id=transaction.id) }}" class="btn btn-outline-danger">Void</a>
{% endif %}
<a href="{{ url_for('pos.transactions') }}" class="btn btn-outline-secondary">Transactions</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Transactions{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="fw-bold mb-0">Transactions</h4>
<input type="date" class="form-control form-control-sm" value="{{ view_date.isoformat() }}"
onchange="window.location='{{ url_for('pos.transactions') }}?date='+this.value"
style="width:150px;">
</div>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Time</th><th>Customer</th><th>Staff</th><th>Total</th><th>Method</th><th>Tip</th><th></th></tr></thead>
<tbody>
{% for t in transactions %}
<tr>
<td class="small">{{ t.created_at.strftime('%I:%M %p') }}</td>
<td class="small">{{ t.customer.name if t.customer else '—' }}</td>
<td class="small">{{ t.staff.name if t.staff else '—' }}</td>
<td class="fw-semibold">${{ "%.2f"|format(t.total) }}</td>
<td class="small">{{ t.payment_method.title() }}</td>
<td class="small">{% if t.tip_amount > 0 %}${{ "%.2f"|format(t.tip_amount) }}{% else %}—{% endif %}</td>
<td><a href="{{ url_for('pos.receipt', transaction_id=t.id) }}" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">No transactions for this day.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Void Transaction{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm border-danger">
<div class="card-header bg-danger text-white fw-semibold">Void Transaction #{{ transaction.id }}</div>
<div class="card-body">
<p class="text-muted">Total: <strong>${{ "%.2f"|format(transaction.total) }}</strong></p>
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate onsubmit="return confirm('Are you sure you want to void this transaction?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Reason <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="reason" required autofocus>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">Confirm Void</button>
<a href="{{ url_for('pos.receipt', transaction_id=transaction.id) }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,42 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Close Day{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">
<i class="bi bi-calculator me-2"></i>Close Day — {{ today.strftime('%B %d, %Y') }}
</div>
<div class="card-body">
<!-- Summary from transactions -->
<table class="table table-sm mb-4">
<tbody>
<tr><td>Cash Sales</td><td class="text-end fw-semibold">${{ "%.2f"|format(total_cash) }}</td></tr>
<tr><td>App Payments (Zelle / Venmo / etc.)</td><td class="text-end">${{ "%.2f"|format(total_app) }}</td></tr>
<tr><td>Tips Collected</td><td class="text-end">${{ "%.2f"|format(total_tips) }}</td></tr>
<tr><td>Gift Card Redemptions</td><td class="text-end">${{ "%.2f"|format(total_gc) }}</td></tr>
<tr class="table-light fw-bold"><td>Expected Cash in Drawer</td><td class="text-end">${{ "%.2f"|format(expected_cash) }}</td></tr>
</tbody>
</table>
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label fw-semibold">Actual Cash Counted ($) <span class="text-danger">*</span></label>
<input type="number" step="0.01" min="0" class="form-control form-control-lg"
name="actual_cash" required autofocus placeholder="0.00">
</div>
<div class="mb-3">
<label class="form-label">Notes <small class="text-muted">(optional)</small></label>
<textarea class="form-control" name="notes" rows="2"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Close Day</button>
<a href="{{ url_for('reconciliation.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,38 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Reconciliation{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-calculator me-2"></i>End-of-Day Reconciliation</h4>
<a href="{{ url_for('reconciliation.close_day') }}" class="btn btn-primary btn-sm">Close Today</a>
</div>
{% if records %}
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Date</th><th>Cash</th><th>App Payments</th><th>Tips</th><th>Gift Cards</th><th>Expected</th><th>Actual</th><th>Variance</th><th>Closed By</th></tr>
</thead>
<tbody>
{% for r in records %}
<tr>
<td class="fw-semibold">{{ r.date.strftime('%Y-%m-%d') }}</td>
<td>${{ "%.2f"|format(r.total_cash) }}</td>
<td>${{ "%.2f"|format(r.total_app_payments) }}</td>
<td>${{ "%.2f"|format(r.total_tips) }}</td>
<td>${{ "%.2f"|format(r.total_gift_card_redemptions) }}</td>
<td>${{ "%.2f"|format(r.expected_cash_in_drawer) }}</td>
<td>${{ "%.2f"|format(r.actual_cash_counted) }}</td>
<td class="fw-semibold {{ 'text-danger' if r.variance < 0 else 'text-success' if r.variance > 0 else '' }}">
{{ '+' if r.variance > 0 else '' }}${{ "%.2f"|format(r.variance) }}
</td>
<td class="small text-muted">{{ r.closed_at.strftime('%I:%M %p') if r.closed_at else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-light">No reconciliation records yet.</div>
{% endif %}
{% endblock %}
+54
View File
@@ -0,0 +1,54 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Reviews{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-star me-2"></i>Customer Reviews</h4>
<div class="text-end">
<div class="fs-3 fw-bold text-warning">{{ avg_rating }} ★</div>
<div class="text-muted small">Average rating</div>
</div>
</div>
<!-- Rating distribution -->
<div class="card shadow-sm mb-4" style="max-width:400px;">
<div class="card-body py-2">
{% for star in [5,4,3,2,1] %}
{% set count = rating_dist.get(star, 0) %}
{% set total = reviews|length %}
<div class="d-flex align-items-center gap-2 mb-1">
<span class="text-muted small" style="width:20px;">{{ star }}★</span>
<div class="progress flex-grow-1" style="height:8px;">
<div class="progress-bar bg-warning"
style="width:{{ ((count / total * 100) | int) if total else 0 }}%"></div>
</div>
<span class="text-muted small" style="width:30px;">{{ count }}</span>
</div>
{% endfor %}
</div>
</div>
{% if reviews %}
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Date</th><th>Rating</th><th>Customer</th><th>Staff</th><th>Comment</th></tr></thead>
<tbody>
{% for r in reviews %}
<tr>
<td class="small text-muted">{{ r.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<span class="text-warning fw-bold">{{ r.rating }}★</span>
</td>
<td class="small">{{ r.customer.name if r.customer else '—' }}</td>
<td class="small">{{ r.staff.name if r.staff else '—' }}</td>
<td class="small">{{ r.comment or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-light">No reviews yet.</div>
{% endif %}
{% endblock %}
+102
View File
@@ -0,0 +1,102 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Services & Products{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-card-list me-2"></i>Services & Products</h4>
<div class="d-flex gap-2">
<a href="{{ url_for('services.create_service') }}" class="btn btn-primary btn-sm">+ Service</a>
<a href="{{ url_for('services.create_product') }}" class="btn btn-outline-primary btn-sm">+ Product</a>
<a href="{{ url_for('services.create_promotion') }}" class="btn btn-outline-warning btn-sm">+ Promotion</a>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Services</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Name</th><th>Category</th><th>Duration</th><th>Price</th><th></th></tr></thead>
<tbody>
{% for s in services %}
<tr class="{{ '' if s.is_active else 'text-muted' }}">
<td class="fw-semibold">{{ s.name }}</td>
<td class="small">{{ s.category or '—' }}</td>
<td class="small">{{ s.duration_min }} min</td>
<td>${{ "%.2f"|format(s.price) }}</td>
<td>
<a href="{{ url_for('services.edit_service', service_id=s.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<form method="POST" action="{{ url_for('services.delete_service', service_id=s.id) }}" class="d-inline"
onsubmit="return confirm('Delete this service?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-danger btn-sm">Del</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted py-3">No services yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Products</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Name</th><th>SKU</th><th>Price</th><th></th></tr></thead>
<tbody>
{% for p in products %}
<tr class="{{ '' if p.is_active else 'text-muted' }}">
<td class="fw-semibold">{{ p.name }}</td>
<td class="small text-muted">{{ p.sku or '—' }}</td>
<td>${{ "%.2f"|format(p.sale_price) }}</td>
<td>
<a href="{{ url_for('services.edit_product', product_id=p.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<form method="POST" action="{{ url_for('services.delete_product', product_id=p.id) }}" class="d-inline"
onsubmit="return confirm('Delete this product?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-danger btn-sm">Del</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="4" class="text-center text-muted py-3">No products yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if promotions %}
<div class="card shadow-sm mt-3 border-warning">
<div class="card-header fw-semibold bg-warning-subtle">Active Promotions</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Name</th><th>Discount</th><th>Applies To</th><th>Ends</th><th></th></tr></thead>
<tbody>
{% for promo in promotions %}
<tr>
<td class="fw-semibold">{{ promo.name }}</td>
<td>{{ promo.discount_percent }}%</td>
<td class="small">{{ promo.applies_to }}</td>
<td class="small text-muted">{{ promo.ends_at.strftime('%Y-%m-%d') }}</td>
<td>
<form method="POST" action="{{ url_for('services.toggle_promotion', promo_id=promo.id) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-{{ 'danger' if promo.is_active else 'success' }} btn-sm">
{{ 'Deactivate' if promo.is_active else 'Activate' }}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,50 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Product{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Product</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name"
value="{{ product.name if product else '' }}" required autofocus>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">SKU</label>
<input type="text" class="form-control" name="sku"
value="{{ product.sku if product else '' }}">
</div>
<div class="col">
<label class="form-label">Category</label>
<input type="text" class="form-control" name="category"
value="{{ product.category if product else '' }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">Sale Price ($) <span class="text-danger">*</span></label>
<input type="number" step="0.01" class="form-control" name="sale_price" min="0"
value="{{ "%.2f"|format(product.sale_price) if product else '' }}" required>
</div>
{% if mode == 'edit' %}
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="is_active" {{ 'checked' if product and product.is_active }}>
<label class="form-check-label" for="is_active">Active</label>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('services.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,84 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}New Promotion{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">New Promotion</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Promotion Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" required autofocus>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Discount % <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="discount_percent" min="1" max="100" required>
</div>
<div class="col">
<label class="form-label">Applies To <span class="text-danger">*</span></label>
<select class="form-select" name="applies_to" id="applies_to"
onchange="toggleTargets(this.value)">
<option value="all_services">All Services</option>
<option value="all_products">All Products</option>
<option value="all">All Services & Products</option>
<option value="service">Specific Services</option>
<option value="product">Specific Products</option>
</select>
</div>
</div>
<div id="target_services" class="mb-3 d-none">
<label class="form-label">Select Services</label>
{% for svc in services %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="target_ids"
value="{{ svc.id }}" id="svc_{{ svc.id }}">
<label class="form-check-label" for="svc_{{ svc.id }}">
{{ svc.name }} — ${{ "%.2f"|format(svc.price) }}
</label>
</div>
{% endfor %}
</div>
<div id="target_products" class="mb-3 d-none">
<label class="form-label">Select Products</label>
{% for p in products %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="target_ids"
value="{{ p.id }}" id="prod_{{ p.id }}">
<label class="form-check-label" for="prod_{{ p.id }}">
{{ p.name }} — ${{ "%.2f"|format(p.sale_price) }}
</label>
</div>
{% endfor %}
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Starts At <span class="text-danger">*</span></label>
<input type="datetime-local" class="form-control" name="starts_at" required>
</div>
<div class="col">
<label class="form-label">Ends At <span class="text-danger">*</span></label>
<input type="datetime-local" class="form-control" name="ends_at" required>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-warning">Create Promotion</button>
<a href="{{ url_for('services.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleTargets(val) {
document.getElementById("target_services").classList.toggle("d-none", val !== "service");
document.getElementById("target_products").classList.toggle("d-none", val !== "product");
}
</script>
{% endblock %}
@@ -0,0 +1,50 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Service{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Service</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name"
value="{{ service.name if service else '' }}" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<input type="text" class="form-control" name="category"
value="{{ service.category if service else '' }}" placeholder="e.g. Nails, Spa, Waxing">
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Duration (min) <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="duration_min" min="5" max="480"
value="{{ service.duration_min if service else 30 }}" required>
</div>
<div class="col">
<label class="form-label">Price ($) <span class="text-danger">*</span></label>
<input type="number" step="0.01" class="form-control" name="price" min="0"
value="{{ "%.2f"|format(service.price) if service else '' }}" required>
</div>
</div>
{% if mode == 'edit' %}
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="is_active" {{ 'checked' if service and service.is_active }}>
<label class="form-check-label" for="is_active">Active</label>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('services.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-7">
<h4 class="fw-bold mb-4"><i class="bi bi-gear me-2"></i>Settings</h4>
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="{{ url_for('settings.save') }}" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<h6 class="fw-semibold text-muted mb-3">Booking</h6>
<div class="mb-3">
<label class="form-label">Advance Booking Days</label>
<input type="number" class="form-control" name="booking_advance_days"
value="{{ settings.get('booking_advance_days', '30') }}" min="1" max="365">
<div class="form-text">How many days ahead customers can book online.</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="auto_confirm_bookings"
value="true" id="auto_confirm"
{{ 'checked' if settings.get('auto_confirm_bookings') == 'true' }}>
<label class="form-check-label" for="auto_confirm">Auto-confirm online bookings</label>
</div>
</div>
<h6 class="fw-semibold text-muted mb-3 mt-4">Reviews</h6>
<div class="mb-3">
<label class="form-label">Review Request Delay (minutes after checkout)</label>
<input type="number" class="form-control" name="review_request_delay_minutes"
value="{{ settings.get('review_request_delay_minutes', '60') }}" min="0">
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Google Review URL</label>
<input type="url" class="form-control" name="google_review_url"
value="{{ settings.get('google_review_url', '') }}" placeholder="https://g.page/…">
</div>
<div class="col">
<label class="form-label">Yelp Review URL</label>
<input type="url" class="form-control" name="yelp_review_url"
value="{{ settings.get('yelp_review_url', '') }}" placeholder="https://yelp.com/…">
</div>
</div>
<h6 class="fw-semibold text-muted mb-3 mt-4">Receipt</h6>
<div class="mb-3">
<label class="form-label">Receipt Footer Note</label>
<textarea class="form-control" name="receipt_footer_note" rows="2">{{ settings.get('receipt_footer_note', '') }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+77
View File
@@ -0,0 +1,77 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Staff Member{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header fw-semibold">{{ 'Edit' if mode == 'edit' else 'New' }} Staff Member</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name"
value="{{ staff.name if staff else '' }}" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Phone <span class="text-danger">*</span></label>
<input type="tel" class="form-control" name="phone"
value="{{ staff.phone if staff else '' }}" required>
</div>
{% if mode == 'create' %}
<div class="mb-3">
<label class="form-label">Passcode (46 digits) <span class="text-danger">*</span></label>
<input type="password" class="form-control" name="passcode"
inputmode="numeric" maxlength="6" required>
<div class="form-text">Staff will use this PIN to log in. Show it to them once, then discard it.</div>
</div>
{% endif %}
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Job Type</label>
<select class="form-select" name="staff_type">
{% for t in ['full_time','part_time','seasonal','receptionist','salon_manager'] %}
<option value="{{ t }}" {{ 'selected' if staff and staff.staff_type == t }}>
{{ t.replace('_',' ').title() }}
</option>
{% endfor %}
</select>
</div>
<div class="col">
<label class="form-label">Pay Type</label>
<select class="form-select" name="pay_type">
{% for t in ['hourly','salary','guarantee'] %}
<option value="{{ t }}" {{ 'selected' if staff and staff.pay_type == t }}>{{ t.title() }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Location Assignments</label>
{% for loc in locations %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="location_ids"
value="{{ loc.id }}" id="loc_{{ loc.id }}"
{{ 'checked' if loc.id in (assigned_ids or set()) }}>
<label class="form-check-label" for="loc_{{ loc.id }}">{{ loc.name }}</label>
</div>
{% endfor %}
</div>
{% if mode == 'edit' %}
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="is_active" {{ 'checked' if staff and staff.is_active }}>
<label class="form-check-label" for="is_active">Active</label>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('staff.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Staff{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-person-badge me-2"></i>Staff</h4>
<a href="{{ url_for('staff.create') }}" class="btn btn-primary btn-sm">+ New Staff Member</a>
</div>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Name</th><th>Phone</th><th>Type</th><th>Pay Type</th><th>Status</th><th>Actions</th></tr></thead>
<tbody>
{% for s in staff_list %}
<tr class="{{ '' if s.is_active else 'text-muted' }}">
<td class="fw-semibold">{{ s.name }}</td>
<td class="small">{{ s.phone }}</td>
<td class="small">{{ s.staff_type.replace('_',' ').title() }}</td>
<td class="small">{{ s.pay_type.title() }}</td>
<td><span class="badge {{ 'bg-success' if s.is_active else 'bg-secondary' }}">{{ 'Active' if s.is_active else 'Inactive' }}</span></td>
<td>
<a href="{{ url_for('staff.edit', staff_id=s.id) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<a href="{{ url_for('staff.reset_passcode', staff_id=s.id) }}" class="btn btn-outline-warning btn-sm">Reset PIN</a>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-4">No staff members yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,28 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Reset Passcode — {{ staff.name }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow-sm border-warning">
<div class="card-header bg-warning-subtle fw-semibold">Reset Passcode — {{ staff.name }}</div>
<div class="card-body">
<p class="text-muted small">Enter a new 46 digit PIN for this staff member. Show it to them once, then discard it.</p>
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">New Passcode</label>
<input type="text" class="form-control form-control-lg text-center font-monospace"
name="passcode" inputmode="numeric" maxlength="6"
placeholder="••••" autofocus required>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-warning">Set New Passcode</button>
<a href="{{ url_for('staff.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,43 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Commission & Pay{% endblock %}
{% block content %}
<h4 class="fw-bold mb-4">Commission & Pay Summary</h4>
{% if pay_periods %}
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">Pay Periods</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Period</th><th>Type</th><th>Base</th><th>Commission</th><th>Total</th><th>Status</th></tr></thead>
<tbody>
{% for pp in pay_periods %}
<tr>
<td class="small">{{ pp.period_start.strftime('%b %d') }}{{ pp.period_end.strftime('%b %d, %Y') }}</td>
<td class="small">{{ pp.pay_type.title() }}</td>
<td>${{ "%.2f"|format(pp.base_amount) }}</td>
<td>${{ "%.2f"|format(pp.commission_amount) }}</td>
<td class="fw-semibold">${{ "%.2f"|format(pp.total_amount) }}</td>
<td><span class="badge bg-{{ {'paid':'success','approved':'primary','draft':'secondary'}.get(pp.status,'secondary') }}">{{ pp.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if commission_logs %}
<div class="card shadow-sm">
<div class="card-header fw-semibold">Recent Commissions</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Period</th><th>Amount</th></tr></thead>
<tbody>
{% for log in commission_logs %}
<tr><td class="small">{{ log.period or '—' }}</td><td>${{ "%.2f"|format(log.amount) }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<a href="{{ url_for('staff_portal.index') }}" class="btn btn-outline-secondary btn-sm mt-3">Back</a>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Staff Portal{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">
<i class="bi bi-person-badge me-2"></i>Welcome, {{ staff.name }}
</h4>
<span class="text-muted small">{{ today.strftime('%A, %B %d') }}</span>
</div>
<!-- Clock in/out -->
<div class="card shadow-sm mb-3 {{ 'border-success' if current_clocking else '' }}">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
{% if current_clocking %}
<span class="badge bg-success fs-6 me-2">On Shift</span>
<span class="text-muted small">Since {{ current_clocking.clocked_in_at.strftime('%I:%M %p') }}</span>
{% else %}
<span class="badge bg-secondary fs-6">Off Shift</span>
{% endif %}
</div>
<div>
{% if current_clocking %}
<form method="POST" action="{{ url_for('staff_portal.clock_out') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-danger">Clock Out</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('staff_portal.clock_in') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-success">Clock In</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Today's appointments -->
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold">
Today's Appointments
<a href="{{ url_for('staff_portal.schedule') }}" class="btn btn-outline-secondary btn-sm float-end">Full Schedule</a>
</div>
{% if todays_appointments %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Time</th><th>Customer</th><th>Service</th><th>Status</th></tr></thead>
<tbody>
{% for a in todays_appointments %}
<tr>
<td class="fw-semibold">{{ a.start_time.strftime('%I:%M %p') }}</td>
<td>{{ a.customer.name if a.customer else 'Walk-in' }}</td>
<td class="small">{{ a.service.name if a.service else '—' }}</td>
<td><span class="badge bg-{{ {'confirmed':'primary','in_progress':'info','pending':'warning'}.get(a.status,'secondary') }}">{{ a.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card-body text-muted small">No appointments scheduled for today.</div>
{% endif %}
</div>
<div class="row g-2">
<div class="col-6"><a href="{{ url_for('staff_portal.commission') }}" class="btn btn-outline-primary w-100">Commission & Pay</a></div>
<div class="col-6"><a href="{{ url_for('staff_portal.profile') }}" class="btn btn-outline-secondary w-100">My Profile</a></div>
</div>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}My Profile{% endblock %}
{% block content %}
<h4 class="fw-bold mb-4">My Profile</h4>
<div class="card shadow-sm" style="max-width:400px;">
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Name</dt><dd class="col-sm-8">{{ staff.name }}</dd>
<dt class="col-sm-4">Phone</dt><dd class="col-sm-8">{{ staff.phone }}</dd>
<dt class="col-sm-4">Type</dt><dd class="col-sm-8">{{ staff.staff_type.replace('_',' ').title() }}</dd>
<dt class="col-sm-4">Pay Type</dt><dd class="col-sm-8">{{ staff.pay_type.title() }}</dd>
</dl>
</div>
</div>
<a href="{{ url_for('staff_portal.index') }}" class="btn btn-outline-secondary btn-sm mt-3">Back</a>
{% endblock %}
@@ -0,0 +1,28 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}My Schedule{% endblock %}
{% block content %}
<h4 class="fw-bold mb-4">My Schedule — Next 7 Days</h4>
{% if appointments %}
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Date</th><th>Time</th><th>Customer</th><th>Service</th><th>Status</th></tr></thead>
<tbody>
{% for a in appointments %}
<tr>
<td class="small">{{ a.start_time.strftime('%b %d') }}</td>
<td class="fw-semibold">{{ a.start_time.strftime('%I:%M %p') }}</td>
<td>{{ a.customer.name if a.customer else 'Walk-in' }}</td>
<td class="small">{{ a.service.name if a.service else '—' }}</td>
<td><span class="badge bg-{{ {'confirmed':'primary','in_progress':'info','pending':'warning'}.get(a.status,'secondary') }}">{{ a.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-light">No upcoming appointments in the next 7 days.</div>
{% endif %}
<a href="{{ url_for('staff_portal.index') }}" class="btn btn-outline-secondary btn-sm mt-3">Back</a>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Add to Waitlist{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Add to Waitlist</div>
<div class="card-body">
{% if error %}<div class="alert alert-danger py-2">{{ error }}</div>{% endif %}
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Customer Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="customer_name" required autofocus>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Phone</label>
<input type="tel" class="form-control" name="customer_phone">
</div>
<div class="col">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="customer_email">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col">
<label class="form-label">Preferred Service</label>
<select class="form-select" name="service_id">
<option value="">— Any —</option>
{% for s in services %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="col">
<label class="form-label">Preferred Staff</label>
<select class="form-select" name="staff_id">
<option value="">— Any —</option>
{% for s in staff_list %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Requested Date</label>
<input type="date" class="form-control" name="requested_date">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Add to Waitlist</button>
<a href="{{ url_for('waitlist.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+55
View File
@@ -0,0 +1,55 @@
{% extends "tenant/layouts/base.html" %}
{% block title %}Waitlist{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0"><i class="bi bi-clock-history me-2"></i>Waitlist</h4>
<a href="{{ url_for('waitlist.add') }}" class="btn btn-primary btn-sm">+ Add to Waitlist</a>
</div>
{% if entries %}
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Name</th><th>Phone</th><th>Service</th><th>Staff</th><th>Requested</th><th>Status</th><th></th></tr>
</thead>
<tbody>
{% for e in entries %}
<tr>
<td class="fw-semibold">{{ e.customer_name }}</td>
<td class="small">{{ e.customer_phone or '—' }}</td>
<td class="small">{{ e.service.name if e.service else '—' }}</td>
<td class="small">{{ e.staff.name if e.staff else '—' }}</td>
<td class="small text-muted">{{ e.requested_date.strftime('%b %d') if e.requested_date else '—' }}</td>
<td>
<span class="badge {{ 'bg-warning text-dark' if e.status == 'waiting' else 'bg-info' }}">
{{ e.status }}
</span>
</td>
<td class="d-flex gap-1">
{% if e.status == 'waiting' %}
<form method="POST" action="{{ url_for('waitlist.notify', entry_id=e.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-primary btn-sm">Notify</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('waitlist.set_status', entry_id=e.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="status" value="booked">
<button class="btn btn-outline-success btn-sm">Booked</button>
</form>
<form method="POST" action="{{ url_for('waitlist.set_status', entry_id=e.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="status" value="expired">
<button class="btn btn-outline-secondary btn-sm">Expire</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-light">No active waitlist entries.</div>
{% endif %}
{% endblock %}