Tier 3: appointments CRUD + time tracking with topbar timer

This commit is contained in:
2026-02-28 04:38:56 +00:00
parent 82d03ce23a
commit 6ad642084d
13 changed files with 1075 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block content %}
<div class="breadcrumb">
<a href="/appointments">Appointments</a>
<span class="sep">/</span>
<span>{{ "Edit" if appointment else "New Appointment" }}</span>
</div>
<div class="page-header">
<h1 class="page-title">{{ "Edit Appointment" if appointment else "New Appointment" }}</h1>
</div>
<div class="card" style="max-width: 720px;">
<form method="POST" action="{{ '/appointments/' ~ appointment.id ~ '/edit' if appointment else '/appointments/create' }}">
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label">Title *</label>
<input type="text" name="title" class="form-input" required value="{{ appointment.title if appointment else '' }}">
</div>
<div class="form-group">
<label class="form-label">Start Date *</label>
<input type="date" name="start_date" class="form-input" required
value="{{ appointment.start_at.strftime('%Y-%m-%d') if appointment and appointment.start_at else '' }}">
</div>
<div class="form-group" id="start-time-group">
<label class="form-label">Start Time</label>
<input type="time" name="start_time" class="form-input" id="start-time-input"
value="{{ appointment.start_at.strftime('%H:%M') if appointment and appointment.start_at and not appointment.all_day else '' }}">
</div>
<div class="form-group">
<label class="form-label">End Date</label>
<input type="date" name="end_date" class="form-input"
value="{{ appointment.end_at.strftime('%Y-%m-%d') if appointment and appointment.end_at else '' }}">
</div>
<div class="form-group" id="end-time-group">
<label class="form-label">End Time</label>
<input type="time" name="end_time" class="form-input" id="end-time-input"
value="{{ appointment.end_at.strftime('%H:%M') if appointment and appointment.end_at and not appointment.all_day else '' }}">
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" name="all_day" id="all-day-check"
{{ 'checked' if appointment and appointment.all_day else '' }}
onchange="toggleAllDay(this.checked)"
style="width: 16px; height: 16px;">
All Day Event
</label>
</div>
<div class="form-group">
<label class="form-label">Recurrence</label>
<select name="recurrence" class="form-select">
<option value="">None</option>
<option value="daily" {{ 'selected' if appointment and appointment.recurrence == 'daily' }}>Daily</option>
<option value="weekly" {{ 'selected' if appointment and appointment.recurrence == 'weekly' }}>Weekly</option>
<option value="monthly" {{ 'selected' if appointment and appointment.recurrence == 'monthly' }}>Monthly</option>
</select>
</div>
<div class="form-group full-width">
<label class="form-label">Location</label>
<input type="text" name="location" class="form-input" placeholder="Address, room, or video link"
value="{{ appointment.location if appointment and appointment.location else '' }}">
</div>
<div class="form-group full-width">
<label class="form-label">Description</label>
<textarea name="description" class="form-textarea" rows="3" placeholder="Notes about this appointment...">{{ appointment.description if appointment and appointment.description else '' }}</textarea>
</div>
<div class="form-group full-width">
<label class="form-label">Tags</label>
<input type="text" name="tags" class="form-input" placeholder="Comma-separated tags"
value="{{ appointment.tags | join(', ') if appointment and appointment.tags else '' }}">
</div>
{% if contacts %}
<div class="form-group full-width">
<label class="form-label">Attendees</label>
<div style="display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; background: var(--surface2); border-radius: var(--radius); border: 1px solid var(--border); max-height: 200px; overflow-y: auto;">
{% for c in contacts %}
<label style="display: flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.85rem; cursor: pointer; white-space: nowrap;">
<input type="checkbox" name="contact_ids" value="{{ c.id }}"
{{ 'checked' if c.id|string in selected_contacts else '' }}
style="width: 14px; height: 14px;">
{{ c.first_name }} {{ c.last_name or '' }}
{% if c.company %}<span style="color: var(--muted); font-size: 0.78rem;">({{ c.company }})</span>{% endif %}
</label>
{% endfor %}
</div>
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ "Update Appointment" if appointment else "Create Appointment" }}</button>
<a href="{{ '/appointments/' ~ appointment.id if appointment else '/appointments' }}" class="btn btn-secondary">Cancel</a>
</div>
</div>
</form>
</div>
<script>
function toggleAllDay(checked) {
const startTime = document.getElementById('start-time-input');
const endTime = document.getElementById('end-time-input');
if (checked) {
startTime.disabled = true;
startTime.style.opacity = '0.4';
endTime.disabled = true;
endTime.style.opacity = '0.4';
} else {
startTime.disabled = false;
startTime.style.opacity = '1';
endTime.disabled = false;
endTime.style.opacity = '1';
}
}
// Init on load
document.addEventListener('DOMContentLoaded', () => {
const cb = document.getElementById('all-day-check');
if (cb && cb.checked) toggleAllDay(true);
});
</script>
{% endblock %}