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 %}