Bo Nix better win the damn super bowl dude he gives me anxiety every time I watch him
This commit is contained in:
39
templates/admin_avail.html
Normal file
39
templates/admin_avail.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<section class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 overflow-x-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold mb-3">Team Availability (Weekly)</h1>
|
||||
<a class="text-sm underline text-slate-300" href="{{ url_for('admin_availability') }}">Refresh</a>
|
||||
</div>
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr>
|
||||
<th class="py-2 text-left">User</th>
|
||||
{% for _k,lab in day_names %}<th class="py-2 text-left">{{ lab }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td class="py-2 font-semibold">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ row.username }}</span>
|
||||
<a href="{{ url_for('admin_availability_edit', user_id=row.user_id) }}"
|
||||
class="text-xs underline text-slate-400 hover:text-slate-200">✎ Edit</a>
|
||||
</div>
|
||||
</td>
|
||||
{% for k,_lab in day_names %}
|
||||
{% set d = row.week.get(k) %}
|
||||
<td class="py-2">
|
||||
{% if d and d.avail %}
|
||||
<span class="inline-flex items-center gap-1 rounded-md border border-emerald-700 bg-emerald-500/15 px-2 py-0.5 text-emerald-200">
|
||||
{{ d.start }}–{{ d.end }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-slate-500">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
75
templates/admin_availability.html
Normal file
75
templates/admin_availability.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<section class="grid gap-6">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<h1 class="text-2xl font-bold">Edit User Availability</h1>
|
||||
<a class="text-sm underline text-slate-300" href="{{ url_for('admin_availability') }}">Back to Team Grid</a>
|
||||
</div>
|
||||
|
||||
<!-- User picker -->
|
||||
<form method="get" class="flex items-center gap-2 max-w-xl">
|
||||
<label class="text-sm text-slate-300">User</label>
|
||||
<select name="user_id" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 w-full">
|
||||
<option value="">— Select a user —</option>
|
||||
{% for u in users %}
|
||||
<option value="{{ u.id }}" {{ 'selected' if (user and user.id==u.id) else '' }}>
|
||||
{{ u.username }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500">Load</button>
|
||||
</form>
|
||||
|
||||
{% if user %}
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Weekly Pattern for: <span class="font-mono">{{ user.username }}</span></h2>
|
||||
<form method="post" class="overflow-x-auto">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="user_id" value="{{ user.id }}">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr>
|
||||
<th class="py-2 pr-4 text-left font-medium">Day</th>
|
||||
<th class="py-2 px-2 text-left font-medium">Available</th>
|
||||
<th class="py-2 px-2 text-left font-medium">Start</th>
|
||||
<th class="py-2 px-2 text-left font-medium">End</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for key,label in day_names %}
|
||||
<tr>
|
||||
<td class="py-2 pr-4"><strong>{{ label }}</strong></td>
|
||||
<td class="py-2 px-2"><input type="checkbox" name="{{ key }}_avail" {{ 'checked' if week[key].avail else '' }}></td>
|
||||
<td class="py-2 px-2"><input type="time" name="{{ key }}_start" value="{{ week[key].start }}" class="px-2 py-1 rounded bg-slate-950 border border-slate-700"></td>
|
||||
<td class="py-2 px-2"><input type="time" name="{{ key }}_end" value="{{ week[key].end }}" class="px-2 py-1 rounded bg-slate-950 border border-slate-700"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">Save for {{ user.username }}</button>
|
||||
<button type="button" onclick="fillAll('06:00','19:00')" class="px-4 py-2 rounded-lg border border-slate-700 hover:border-slate-500">Quick-fill 06:00–19:00</button>
|
||||
<button type="button" onclick="setAllOff()" class="px-4 py-2 rounded-lg border border-slate-700 hover:border-slate-500">Set All Off</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillAll(s,e){
|
||||
const days = ['mon','tue','wed','thu','fri','sat','sun'];
|
||||
days.forEach(d=>{
|
||||
document.querySelector(`input[name="${d}_avail"]`).checked = true;
|
||||
document.querySelector(`input[name="${d}_start"]`).value = s;
|
||||
document.querySelector(`input[name="${d}_end"]`).value = e;
|
||||
});
|
||||
}
|
||||
function setAllOff(){
|
||||
const days = ['mon','tue','wed','thu','fri','sat','sun'];
|
||||
days.forEach(d=>{
|
||||
document.querySelector(`input[name="${d}_avail"]`).checked = false;
|
||||
document.querySelector(`input[name="${d}_start"]`).value = '08:00';
|
||||
document.querySelector(`input[name="${d}_end"]`).value = '17:00';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</section>
|
||||
148
templates/admin_info.html
Normal file
148
templates/admin_info.html
Normal file
@@ -0,0 +1,148 @@
|
||||
<section class="grid gap-8">
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">Info Admin Console</h1>
|
||||
<a href="{{ url_for('info_page') }}" class="text-sm underline">Back to Info</a>
|
||||
</header>
|
||||
|
||||
<!-- Contacts -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h2 class="text-lg font-semibold">Contacts</h2>
|
||||
<form method="post" action="{{ url_for('admin_info_contacts_create') }}" class="mt-3 grid gap-2 sm:grid-cols-[1fr,1fr,160px,100px,100px]">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input name="name" placeholder="Name" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" required>
|
||||
<input name="role" placeholder="Role" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="phone" placeholder="Phone" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="priority" type="number" min="1" max="9" value="5" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700">Add</button>
|
||||
</form>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300"><tr><th class="py-2 text-left px-2">Name</th><th class="py-2 text-left px-2">Role</th><th class="py-2 text-left px-2">Phone</th><th class="py-2 text-left px-2">Priority</th><th class="py-2 text-left px-2">Active</th><th class="py-2 text-left px-2">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for c in contacts %}
|
||||
<tr>
|
||||
<td class="py-2 px-2">{{ c.name }}</td>
|
||||
<td class="py-2 px-2">{{ c.role or '' }}</td>
|
||||
<td class="py-2 px-2">{{ c.phone or '' }}</td>
|
||||
<td class="py-2 px-2">{{ c.priority }}</td>
|
||||
<td class="py-2 px-2">{{ 'yes' if c.is_active else 'no' }}</td>
|
||||
<td class="py-2 px-2">
|
||||
<form method="post" action="{{ url_for('admin_info_contacts_toggle', cid=c.id) }}" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-2 py-1 rounded border border-slate-700 hover:border-slate-500 text-xs">{{ 'Deactivate' if c.is_active else 'Activate' }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin_info_contacts_delete', cid=c.id) }}" class="inline" onsubmit="return confirm('Delete contact?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-2 py-1 rounded border border-red-700 hover:bg-red-700/10 text-xs">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Department Extensions -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h2 class="text-lg font-semibold">Department Extensions</h2>
|
||||
<form method="post" action="{{ url_for('admin_info_exts_create') }}" class="mt-3 grid gap-2 sm:grid-cols-[180px,1fr,120px]">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input name="ext" placeholder="532000" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" required>
|
||||
<input name="dept" placeholder="Service Counter" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" required>
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700">Add</button>
|
||||
</form>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300"><tr><th class="py-2 text-left px-2">Ext</th><th class="py-2 text-left px-2">Department</th><th class="py-2 text-left px-2">Active</th><th class="py-2 text-left px-2">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for d in exts %}
|
||||
<tr>
|
||||
<td class="py-2 px-2 font-mono">{{ d.ext }}</td>
|
||||
<td class="py-2 px-2">{{ d.dept }}</td>
|
||||
<td class="py-2 px-2">{{ 'yes' if d.is_active else 'no' }}</td>
|
||||
<td class="py-2 px-2">
|
||||
<form method="post" action="{{ url_for('admin_info_exts_toggle', did=d.id) }}" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-2 py-1 rounded border border-slate-700 hover:border-slate-500 text-xs">{{ 'Deactivate' if d.is_active else 'Activate' }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin_info_exts_delete', did=d.id) }}" class="inline" onsubmit="return confirm('Delete extension?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-2 py-1 rounded border border-red-700 hover:bg-red-700/10 text-xs">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Support Items -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h2 class="text-lg font-semibold">Support & Escalation</h2>
|
||||
<form method="post" action="{{ url_for('admin_info_support_create') }}" class="mt-3 grid gap-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="grid sm:grid-cols-3 gap-2">
|
||||
<input name="category" placeholder="Guest Services" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="email" placeholder="guestservices@…" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="phone" placeholder="806-791-8181 Option 1" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</div>
|
||||
<textarea name="issues" rows="3" placeholder="One issue per line…" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700"></textarea>
|
||||
<input name="note" placeholder="Notes / disclaimers (optional)" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm inline-flex items-center gap-2">
|
||||
<input type="checkbox" name="admin_only" class="rounded border-slate-700 bg-slate-950">
|
||||
<span>Admin only</span>
|
||||
</label>
|
||||
<button class="ml-auto px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));">
|
||||
{% for s in supports %}
|
||||
<article class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold">{{ s.category }}</h3>
|
||||
<span class="text-xs text-slate-400">{{ s.audience }}</span>
|
||||
</div>
|
||||
{% if s.email %}<p class="text-sm text-slate-300 mt-1">{{ s.email }}</p>{% endif %}
|
||||
{% if s.phone %}<p class="text-sm text-slate-300">{{ s.phone }}</p>{% endif %}
|
||||
{% if s.note %}<p class="text-xs text-slate-400 mt-2">{{ s.note }}</p>{% endif %}
|
||||
{% if s.issues() %}
|
||||
<ul class="mt-3 text-sm list-disc pl-5 space-y-1">{% for it in s.issues() %}<li>{{ it }}</li>{% endfor %}</ul>
|
||||
{% endif %}
|
||||
<div class="mt-3 flex gap-2">
|
||||
<form method="post" action="{{ url_for('admin_info_support_toggle', sid=s.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-2 py-1 rounded border border-slate-700 hover:border-slate-500 text-xs">{{ 'Deactivate' if s.is_active else 'Activate' }}</button></form>
|
||||
<form method="post" action="{{ url_for('admin_info_support_delete', sid=s.id) }}" onsubmit="return confirm('Delete support item?');"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-2 py-1 rounded border border-red-700 hover:bg-red-700/10 text-xs">Delete</button></form>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin-Only Quick Notes -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h2 class="text-lg font-semibold">Admin Quick Notes (Secrets)</h2>
|
||||
<form method="post" action="{{ url_for('admin_info_secret_create') }}" class="mt-3 grid gap-2 sm:grid-cols-[1fr,1fr]">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input name="label" placeholder="Register Login" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="value" placeholder="#291 / 0000" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<textarea name="notes" rows="2" placeholder="Notes (optional)" class="sm:col-span-2 px-3 py-2 rounded-lg bg-slate-950 border border-slate-700"></textarea>
|
||||
<div class="sm:col-span-2"><button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700">Add</button></div>
|
||||
</form>
|
||||
<div class="mt-4 grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
{% for s in secrets %}
|
||||
<article class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<h3 class="font-semibold">{{ s.label }}</h3>
|
||||
<p class="mt-1 font-mono text-sm">{{ s.value }}</p>
|
||||
{% if s.notes %}<p class="text-xs text-slate-400 mt-2 whitespace-pre-wrap">{{ s.notes }}</p>{% endif %}
|
||||
<div class="mt-3 flex gap-2">
|
||||
<form method="post" action="{{ url_for('admin_info_secret_toggle', sid=s.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-2 py-1 rounded border border-slate-700 hover:border-slate-500 text-xs">{{ 'Deactivate' if s.is_active else 'Activate' }}</button></form>
|
||||
<form method="post" action="{{ url_for('admin_info_secret_delete', sid=s.id) }}" onsubmit="return confirm('Delete note?');"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-2 py-1 rounded border border-red-700 hover:bg-red-700/10 text-xs">Delete</button></form>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
32
templates/admin_reqs_off.html
Normal file
32
templates/admin_reqs_off.html
Normal file
@@ -0,0 +1,32 @@
|
||||
TPL_ADMIN_REQS_BODY = """
|
||||
<section class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 overflow-x-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold mb-3">Time-off Requests</h1>
|
||||
<a class="text-sm underline text-slate-300" href="{{ url_for('admin_requests_export') }}">Export CSV</a>
|
||||
</div>
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr><th class="py-2 text-left">User</th><th class="py-2 text-left">Date</th><th class="py-2 text-left">Status</th><th class="py-2 text-left">Note</th><th class="py-2 text-left">Requested</th><th class="py-2 text-left">Action</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td class="py-2">{{ r.user.username }}</td>
|
||||
<td class="py-2">{{ r.date }}</td>
|
||||
<td class="py-2 capitalize">{{ r.status }}</td>
|
||||
<td class="py-2">{{ r.note or '' }}</td>
|
||||
<td class="py-2">{{ r.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td class="py-2">
|
||||
{% if r.status == 'pending' %}
|
||||
<div class="flex gap-2">
|
||||
<form method="post" action="{{ url_for('admin_requests_action', req_id=r.id, action='approve') }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-700" type="submit">Approve</button></form>
|
||||
<form method="post" action="{{ url_for('admin_requests_action', req_id=r.id, action='deny') }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" type="submit">Deny</button></form>
|
||||
</div>
|
||||
{% else %}<span class="text-slate-400">—</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
"""
|
||||
80
templates/admin_secret_santa.html
Normal file
80
templates/admin_secret_santa.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<section class="grid gap-6">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Secret Santa</h1>
|
||||
<a class="text-sm underline text-slate-300" href="{{ url_for('admin_secret_santa_export') }}">Export CSV</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="ssFilter"
|
||||
placeholder="Filter by username or name…"
|
||||
class="w-full sm:w-[28rem] px-4 py-2.5 rounded-xl bg-slate-950 border border-slate-700"
|
||||
oninput="filterSS(this.value)">
|
||||
</div>
|
||||
|
||||
<div id="ssGrid" class="grid gap-6"
|
||||
style="grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));">
|
||||
{% for e in rows %}
|
||||
<article
|
||||
class="ss-card group rounded-3xl border border-slate-800 bg-slate-900/60 p-6 hover:border-brand-600/50 hover:shadow-xl hover:shadow-brand-600/10 transition"
|
||||
data-keywords="{{ (e.user.username ~ ' ' ~ (e.full_name or '') ) | lower }}"
|
||||
>
|
||||
<header class="flex items-start justify-between gap-4">
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-lg font-semibold leading-6">{{ e.full_name or '—' }}</h3>
|
||||
<p class="text-xs text-slate-400 break-all leading-5">{{ e.user.username }}</p>
|
||||
</div>
|
||||
<span
|
||||
class="shrink-0 inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs
|
||||
{{ 'border-emerald-700 bg-emerald-500/15 text-emerald-200' if e.jewelry else 'border-slate-700 bg-slate-800/60 text-slate-300' }}">
|
||||
{{ 'Jewelry: Yes' if e.jewelry else 'Jewelry: No' }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="mt-5 grid grid-cols-2 gap-x-6 gap-y-3 text-sm leading-6">
|
||||
<div>
|
||||
<p class="text-slate-400 text-xs">Age</p>
|
||||
<p class="font-medium">{{ e.age or '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 text-xs">Birthday</p>
|
||||
<p class="font-medium">{{ e.birthday or '—' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-slate-400 text-xs">Favorite Gift Card</p>
|
||||
<p class="font-medium">{{ e.gift_card or '—' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-slate-400 text-xs">Favorite Type of Movie</p>
|
||||
<p class="font-medium">{{ e.fav_movie or '—' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-slate-400 text-xs">Hobbies</p>
|
||||
<p class="whitespace-pre-wrap">{{ e.hobbies or '—' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-6 pt-4 border-t border-slate-800 flex items-center justify-between text-xs text-slate-400 leading-6">
|
||||
<span>Updated {{ e.updated_at.strftime('%Y-%m-%d %H:%M') if e.updated_at else '—' }}</span>
|
||||
<span class="opacity-70">ID #{{ e.id }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not rows %}
|
||||
<p class="text-slate-400 text-sm">No Secret Santa entries yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function filterSS(q){
|
||||
q = (q || '').toLowerCase();
|
||||
document.querySelectorAll('.ss-card').forEach(card=>{
|
||||
const keys = card.getAttribute('data-keywords') || '';
|
||||
card.style.display = keys.includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
100
templates/admin_users.html
Normal file
100
templates/admin_users.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<section class="grid gap-6">
|
||||
<!-- Create User -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-5">
|
||||
<h1 class="text-xl font-bold">Users</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Temp password will be generated and shown as a flash message.</p>
|
||||
|
||||
<form method="post" action="{{ url_for('admin_users_create') }}"
|
||||
class="mt-4 grid gap-3 sm:grid-cols-[1fr,200px,140px]">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input name="username" placeholder="Username (e.g. email)"
|
||||
required
|
||||
class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-brand-600" />
|
||||
<select name="role"
|
||||
class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-brand-600">
|
||||
<option value="member">Member</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Users table -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-5 overflow-x-auto">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h2 class="text-lg font-semibold">All Users</h2>
|
||||
<input id="userFilter" placeholder="Filter by username…" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" oninput="filterUsers(this.value)" />
|
||||
</div>
|
||||
<table class="min-w-full text-sm mt-3">
|
||||
<thead class="text-slate-300">
|
||||
<tr>
|
||||
<th class="py-2 pr-3 text-left font-medium">ID</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Username</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Role</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Active</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Must Change PW</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for u in users %}
|
||||
<tr id="u{{ u.id }}">
|
||||
<td class="py-2 pr-3 text-slate-300">{{ u.id }}</td>
|
||||
<td class="py-2 pr-3 font-medium">{{ u.username }}</td>
|
||||
|
||||
<!-- Role updater -->
|
||||
<td class="py-2 pr-3">
|
||||
<form method="post" action="{{ url_for('admin_users_role', user_id=u.id) }}" class="flex items-center gap-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<select name="role"
|
||||
class="px-2 py-1 rounded bg-slate-950 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-brand-600">
|
||||
<option value="member" {{ 'selected' if u.role=='member' else '' }}>member</option>
|
||||
<option value="admin" {{ 'selected' if u.role=='admin' else '' }}>admin</option>
|
||||
</select>
|
||||
<button class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" type="submit">
|
||||
Update
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<!-- Active toggle -->
|
||||
<td class="py-2 pr-3">
|
||||
<form method="post" action="{{ url_for('admin_users_toggle', user_id=u.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-lg {{ 'bg-emerald-600 hover:bg-emerald-700' if u.is_active else 'bg-slate-700 hover:bg-slate-600' }}"
|
||||
type="submit">
|
||||
{{ 'Active' if u.is_active else 'Inactive' }}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td class="py-2 pr-3">{{ 'yes' if u.must_change_password else 'no' }}</td>
|
||||
|
||||
<!-- Reset -->
|
||||
<td class="py-2 pr-3">
|
||||
<form method="post" action="{{ url_for('admin_users_reset', user_id=u.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-3 py-1.5 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">
|
||||
Reset Password
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
function filterUsers(q) {
|
||||
q = (q || '').toLowerCase();
|
||||
document.querySelectorAll('tbody tr').forEach(tr => {
|
||||
const name = tr.querySelector('td:nth-child(2)')?.textContent.toLowerCase() || '';
|
||||
tr.style.display = name.includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
22
templates/admin_wishlists.html
Normal file
22
templates/admin_wishlists.html
Normal file
@@ -0,0 +1,22 @@
|
||||
TPL_ADMIN_WISHLISTS_BODY = """
|
||||
<section class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 overflow-x-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold mb-3">Secret Santa / Wishlists</h1>
|
||||
<a class="text-sm underline text-slate-300" href="{{ url_for('admin_secret_santa_export') }}">Export Secret Santa CSV</a>
|
||||
</div>
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr><th class="py-2 text-left">User</th><th class="py-2 text-left">Last Updated</th><th class="py-2 text-left">Wishlist</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for w in rows %}
|
||||
<tr>
|
||||
<td class="py-2">{{ w.user.username }}</td>
|
||||
<td class="py-2">{{ w.updated_at.strftime('%Y-%m-%d %H:%M') if w.updated_at else '' }}</td>
|
||||
<td class="py-2 whitespace-pre-wrap">{{ w.wishlist }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
"""
|
||||
211
templates/base.html
Normal file
211
templates/base.html
Normal file
@@ -0,0 +1,211 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full bg-slate-950">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>{{ title or app_brand }}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Every box prints on its own page */
|
||||
@media print {
|
||||
.print-block {
|
||||
page-break-before: always !important;
|
||||
break-before: page !important;
|
||||
}
|
||||
|
||||
/* remove first blank page */
|
||||
.print-block:first-child {
|
||||
page-break-before: avoid !important;
|
||||
break-before: avoid !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Base print reset */
|
||||
@media print {
|
||||
html, body {
|
||||
background: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Hide app chrome */
|
||||
header,
|
||||
nav,
|
||||
footer,
|
||||
.fixed,
|
||||
#mobileNav,
|
||||
#navToggle {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Remove Tailwind background classes */
|
||||
.bg-slate-950,
|
||||
.bg-slate-900\/60,
|
||||
.bg-slate-950\/60 {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-slate-100,
|
||||
.text-slate-200,
|
||||
.text-slate-300,
|
||||
.text-slate-400 {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Force links to print black */
|
||||
a {
|
||||
color: #000000 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* Expand content full width */
|
||||
main {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Page breaks for nice sections */
|
||||
.print-section {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
/* Avoid truncation */
|
||||
.no-break {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body class="h-full text-slate-100 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
|
||||
<div class="min-h-screen">
|
||||
|
||||
<!-- Topbar -->
|
||||
<header class="sticky top-0 z-50 backdrop-blur bg-slate-950/70 border-b border-slate-800">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="h-14 flex items-center justify-between">
|
||||
<!-- Brand -->
|
||||
<a href="{{ url_for('dashboard') }}" class="flex items-center gap-2">
|
||||
<div class="h-7 w-7 rounded bg-brand-600/20 border border-brand-600/30 grid place-items-center font-bold">SC</div>
|
||||
<span class="font-semibold tracking-wide">{{ app_brand }}</span>
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden md:flex items-center gap-2 text-sm">
|
||||
<a class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('dashboard') }}">Home</a>
|
||||
<a class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('availability') }}">Your Availability</a>
|
||||
<a class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('request_off') }}">Request Off</a>
|
||||
<a class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('secret_santa') }}">Secret Santa</a>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<a class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('admin_users') }}">Admin</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('logout') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-3 py-1.5 rounded-lg bg-brand-600 text-white hover:bg-brand-700" type="submit">Logout</button>
|
||||
</form>
|
||||
</nav>
|
||||
<!-- Mobile menu -->
|
||||
<button id="navToggle" class="md:hidden inline-flex items-center justify-center h-9 w-9 rounded-lg border border-slate-700 hover:border-slate-500" aria-label="Open menu" aria-expanded="false" aria-controls="mobileNav">
|
||||
<svg id="navIconOpen" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
<svg id="navIconClose" class="h-5 w-5 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile nav panel -->
|
||||
<div id="mobileNav" class="md:hidden max-w-6xl mx-auto px-4 pb-3 hidden">
|
||||
<div class="grid gap-2 text-sm">
|
||||
<a class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('dashboard') }}">Home</a>
|
||||
<a class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('availability') }}">Availability</a>
|
||||
<a class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('request_off') }}">Request Off</a>
|
||||
<a class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('secret_santa') }}">Secret Santa</a>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<a class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500" href="{{ url_for('admin_users') }}">Admin</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('logout') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="px-3 py-2 rounded-lg bg-brand-600 text-white hover:bg-brand-700" type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="max-w-6xl mx-auto px-4 pt-6 pb-[6.5rem]"> <!-- ⬅️ room for bottom nav + iOS inset -->
|
||||
{% with msgs = get_flashed_messages(with_categories=true) %}
|
||||
{% if msgs %}
|
||||
<div class="space-y-2 mb-4">
|
||||
{% for cat,msg in msgs %}
|
||||
<div class="rounded-lg px-3 py-2 text-sm border {{ 'bg-emerald-500/15 border-emerald-700 text-emerald-200' if cat=='ok' else 'bg-red-500/15 border-red-700 text-red-200' }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{{ content|safe }}
|
||||
</main>
|
||||
|
||||
<!-- Bottom nav -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="fixed bottom-0 inset-x-0 z-50 bg-[#111827] border-t border-gray-800 md:hidden pb-[env(safe-area-inset-bottom)]">
|
||||
<div class="flex justify-around text-sm text-gray-400">
|
||||
<a href="{{ url_for('dashboard') }}" class="flex flex-col items-center justify-center py-2 px-3 {% if 'dashboard' in request.path %}text-white{% endif %}">
|
||||
<svg class="w-5 h-5 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M3 12l2-2 7-7 7 7 2 2"/></svg>
|
||||
Home
|
||||
</a>
|
||||
<a href="{{ url_for('availability') }}" class="flex flex-col items-center justify-center py-2 px-3 {% if 'availability' in request.path %}text-white{% endif %}">
|
||||
<svg class="w-5 h-5 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M8 7V3m8 4V3M3 11h18"/></svg>
|
||||
Availability
|
||||
</a>
|
||||
<a href="{{ url_for('request_off') }}" class="flex flex-col items-center justify-center py-2 px-3 {% if 'request' in request.path %}text-white{% endif %}">
|
||||
<svg class="w-5 h-5 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M9 5v6m4 0h6"/></svg>
|
||||
Requests
|
||||
</a>
|
||||
<a href="{{ url_for('secret_santa') }}" class="flex flex-col items-center justify-center py-2 px-3 {% if 'secret' in request.path %}text-white{% endif %}">
|
||||
<svg class="w-5 h-5 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5z"/></svg>
|
||||
Gifts
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<footer class="text-xs text-center text-slate-500 py-6">© {{ app_brand }}</footer>
|
||||
</div>
|
||||
|
||||
<!-- Nav toggle -->
|
||||
<script>
|
||||
(function () {
|
||||
const btn = document.getElementById('navToggle');
|
||||
const menu = document.getElementById('mobileNav');
|
||||
const openI = document.getElementById('navIconOpen');
|
||||
const closeI = document.getElementById('navIconClose');
|
||||
if (!btn || !menu) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const isHidden = menu.classList.contains('hidden');
|
||||
menu.classList.toggle('hidden');
|
||||
btn.setAttribute('aria-expanded', String(isHidden));
|
||||
openI.classList.toggle('hidden', !isHidden);
|
||||
closeI.classList.toggle('hidden', isHidden);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
34
templates/tpl_avail.html
Normal file
34
templates/tpl_avail.html
Normal file
@@ -0,0 +1,34 @@
|
||||
TPL_AVAIL_BODY = """
|
||||
<section class="grid gap-6">
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h1 class="text-xl font-bold mb-3">Your Weekly Availability</h1>
|
||||
<form method="post" class="overflow-x-auto">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr>
|
||||
<th class="py-2 pr-4 text-left font-medium">Day</th>
|
||||
<th class="py-2 px-2 text-left font-medium">Available</th>
|
||||
<th class="py-2 px-2 text-left font-medium">Start</th>
|
||||
<th class="py-2 px-2 text-left font-medium">End</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for key,label in day_names %}
|
||||
<tr>
|
||||
<td class="py-2 pr-4"><strong>{{ label }}</strong></td>
|
||||
<td class="py-2 px-2"><input type="checkbox" name="{{ key }}_avail" {{ 'checked' if week[key].avail else '' }}></td>
|
||||
<td class="py-2 px-2"><input type="time" name="{{ key }}_start" value="{{ week[key].start }}" class="px-2 py-1 rounded bg-slate-950 border border-slate-700"></td>
|
||||
<td class="py-2 px-2"><input type="time" name="{{ key }}_end" value="{{ week[key].end }}" class="px-2 py-1 rounded bg-slate-950 border border-slate-700"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">Save Availability</button>
|
||||
<span class="text-xs text-slate-400">Tip: Use the links in the header to request a specific day off or fill out Secret Santa.</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
13
templates/tpl_change_password.html
Normal file
13
templates/tpl_change_password.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<h1 class="text-xl font-bold mb-3">Change Password</h1>
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 max-w-md">
|
||||
<form method="post" class="grid gap-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input name="current_password" type="password" placeholder="Current password" required autocomplete="current-password" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="new_password" type="password" placeholder="New password (min 10 chars)" required autocomplete="new-password" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<input name="confirm_password" type="password" placeholder="Confirm new password" required autocomplete="new-password" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">Update</button>
|
||||
</form>
|
||||
{% if user.must_change_password %}
|
||||
<p class="text-xs text-slate-400 mt-2">You must set a new password before continuing.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
130
templates/tpl_dashboard_body.html
Normal file
130
templates/tpl_dashboard_body.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<section class="grid gap-4">
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold">Welcome, {{ user.username }}</h1>
|
||||
<p class="text-slate-300">Role: <span class="font-semibold uppercase">{{ user.role }}</span></p>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-brand-600 opacity-80" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Member tiles -->
|
||||
|
||||
<a href="{{ url_for('info_page') }}" class="group relative rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 focus:ring-offset-slate-950 col-span-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Department / Store Info</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Phones, extensions, support contacts, and notes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('availability') }}" class="group relative rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 focus:ring-offset-slate-950">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10m-7 4h4m-9 5h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v11a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Your Availability</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Set your weekly pattern.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('request_off') }}" class="group relative rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 17l4 4 4-4m0-5l-4-4-4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Request Off</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Ask for specific days off.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('secret_santa') }}" class="group relative rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.5 0-3 1-3 3 0 1.1.9 2 2 2h2v1a2 2 0 01-2 2H8m4-8c1.5 0 3 1 3 3 0 1.1-.9 2-2 2h-2v1a2 2 0 002 2h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Secret Santa</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Fill out your gift preferences.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{% if user.role == 'admin' %}
|
||||
<!-- Admin tiles -->
|
||||
<a href="{{ url_for('admin_availability') }}" class="group rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Team Availability</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">View weekly patterns by user.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin_requests') }}" class="group rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Time-off Requests</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Approve or deny requests.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin_secret_santa') }}" class="group rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 22s8-4 8-10V5a8 8 0 10-16 0v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Secret Santa (Admin)</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Browse entries and export data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin_users') }}" class="group rounded-2xl border border-slate-800 bg-slate-900/60 p-5 hover:border-brand-600/60 hover:shadow-xl hover:shadow-brand-600/10 transition-all duration-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-xl bg-brand-600/10 border border-brand-600/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A4 4 0 017 17h10a4 4 0 011.879.804M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">User Management</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Create accounts and set roles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
93
templates/tpl_info_page.html
Normal file
93
templates/tpl_info_page.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<section class="grid gap-6">
|
||||
<header class="flex items-center justify-between flex-wrap gap-3 print:hidden">
|
||||
<h1 class="text-2xl font-bold">Department / Store Info</h1>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<a href="{{ url_for('admin_info_console') }}" class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500 text-sm">Admin Console</a>
|
||||
{% endif %}
|
||||
<button onclick="window.print()" class="px-3 py-2 rounded-lg border border-slate-700 hover:border-slate-500 text-sm">
|
||||
Print / PDF
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Quick Contacts -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 print-block">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-800">
|
||||
<h2 class="text-lg font-semibold">Quick Contacts</h2>
|
||||
<input id="q" placeholder="Filter by name…" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 text-sm" oninput="filterContacts(this.value)">
|
||||
</div>
|
||||
<div id="contactsGrid" class="p-4 grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
|
||||
{% for c in contacts %}
|
||||
<article class="contact-card rounded-xl border border-slate-800 bg-slate-950/60 p-3" data-key="{{ (c.name ~ ' ' ~ (c.role or '') ~ ' ' ~ (c.phone or '')) | lower }}">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold">{{ c.name }}</h3>
|
||||
{% if c.priority == 1 %}<span class="text-xs px-2 py-0.5 rounded-full border border-amber-600 text-amber-200">Priority</span>{% endif %}
|
||||
</div>
|
||||
{% if c.role %}<p class="text-xs text-slate-400">{{ c.role }}</p>{% endif %}
|
||||
{% if c.phone %}<p class="mt-1 text-sm font-mono">{{ c.phone }}</p>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Department Extensions -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 overflow-x-auto print-block">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-lg font-semibold">Department Extensions</h2>
|
||||
</div>
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300"><tr><th class="py-2 px-3 text-left font-medium">Extension</th><th class="py-2 px-3 text-left font-medium">Department</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for d in exts %}
|
||||
<tr><td class="py-2 px-3 font-mono">{{ d.ext }}</td><td class="py-2 px-3">{{ d.dept }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Support Items -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 print-block">
|
||||
<div class="p-4 border-b border-slate-800"><h2 class="text-lg font-semibold">Support & Escalation</h2></div>
|
||||
<div class="p-4 grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));">
|
||||
{% for s in supports %}
|
||||
<article class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<h3 class="font-semibold">{{ s.category }}</h3>
|
||||
{% if s.email %}<p class="text-sm text-slate-300 mt-1"><span class="text-slate-400">Email:</span> {{ s.email }}</p>{% endif %}
|
||||
{% if s.phone %}<p class="text-sm text-slate-300"><span class="text-slate-400">Phone:</span> {{ s.phone }}</p>{% endif %}
|
||||
{% if s.note %}<p class="text-xs text-slate-400 mt-2">{{ s.note }}</p>{% endif %}
|
||||
{% if s.issues() %}
|
||||
<ul class="mt-3 text-sm list-disc pl-5 space-y-1">
|
||||
{% for it in s.issues() %}<li>{{ it }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if admin_secrets %}
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 print-block">
|
||||
<div class="p-4 border-b border-slate-800 flex items-center justify-between ">
|
||||
<h2 class="text-lg font-semibold">Admin Quick Notes</h2>
|
||||
<span class="text-xs text-slate-400">Visible to admins only</span>
|
||||
</div>
|
||||
<div class="p-4 grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));">
|
||||
{% for s in admin_secrets %}
|
||||
<article class="rounded-xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<h3 class="font-semibold">{{ s.label }}</h3>
|
||||
<p class="mt-1 font-mono text-sm">{{ s.value }}</p>
|
||||
{% if s.notes %}<p class="text-xs text-slate-400 mt-2 whitespace-pre-wrap">{{ s.notes }}</p>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function filterContacts(q){
|
||||
q = (q||'').toLowerCase();
|
||||
document.querySelectorAll('.contact-card').forEach(c=>{
|
||||
c.style.display = (c.getAttribute('data-key')||'').includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
18
templates/tpl_login_body.html
Normal file
18
templates/tpl_login_body.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<section class="max-w-md mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-3">Sign in</h1>
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<form method="post" class="grid gap-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Username</span>
|
||||
<input name="username" autocomplete="username" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Password</span>
|
||||
<input name="password" type="password" autocomplete="current-password" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
<button class="mt-2 px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">Login</button>
|
||||
<p class="text-xs text-slate-400">First run? Use the bootstrap admin, then change it.</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
93
templates/tpl_request_off_body.html
Normal file
93
templates/tpl_request_off_body.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<section class="grid gap-6">
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h1 class="text-xl font-bold mb-3">Request Time Off</h1>
|
||||
<p class="text-sm text-slate-400 mb-4">
|
||||
Use this page to request specific days off. Your leader will approve or deny
|
||||
the request in the admin view.
|
||||
</p>
|
||||
|
||||
<!-- New request form -->
|
||||
<form method="post" action="{{ url_for('requests_new') }}" class="grid gap-3 max-w-md">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Date</span>
|
||||
<input type="date"
|
||||
name="date"
|
||||
required
|
||||
class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" />
|
||||
</label>
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Note (optional)</span>
|
||||
<textarea name="note"
|
||||
rows="3"
|
||||
maxlength="240"
|
||||
class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700"
|
||||
placeholder="Reason, details, or anything the scheduler should know."></textarea>
|
||||
</label>
|
||||
<button class="mt-1 px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold"
|
||||
type="submit">
|
||||
Submit Request
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Existing requests -->
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 overflow-x-auto">
|
||||
<h2 class="text-lg font-semibold mb-3">Your Requests</h2>
|
||||
{% if my_reqs %}
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-300">
|
||||
<tr>
|
||||
<th class="py-2 pr-3 text-left font-medium">Date</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Status</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Note</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Requested</th>
|
||||
<th class="py-2 pr-3 text-left font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
{% for r in my_reqs %}
|
||||
<tr>
|
||||
<td class="py-2 pr-3">{{ r.date }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs capitalize
|
||||
{% if r.status == 'pending' %}
|
||||
bg-amber-500/15 text-amber-200 border border-amber-600
|
||||
{% elif r.status == 'approved' %}
|
||||
bg-emerald-500/15 text-emerald-200 border border-emerald-600
|
||||
{% elif r.status == 'denied' %}
|
||||
bg-red-500/15 text-red-200 border border-red-600
|
||||
{% else %}
|
||||
bg-slate-700/40 text-slate-200 border border-slate-600
|
||||
{% endif %}">
|
||||
{{ r.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 pr-3 whitespace-pre-wrap">{{ r.note or '' }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
{{ r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '' }}
|
||||
</td>
|
||||
<td class="py-2 pr-3">
|
||||
{% if r.status == 'pending' %}
|
||||
<form method="post"
|
||||
action="{{ url_for('requests_cancel', req_id=r.id) }}"
|
||||
onsubmit="return confirm('Cancel this request?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit"
|
||||
class="px-3 py-1.5 rounded-lg border border-slate-700 hover:border-slate-500 text-xs">
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-xs text-slate-500">No action</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm text-slate-400">You don't have any requests yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
48
templates/tpl_secret_santa.html
Normal file
48
templates/tpl_secret_santa.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<section class="grid gap-6">
|
||||
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h1 class="text-xl font-bold mb-3">Secret Santa</h1>
|
||||
<form method="post" class="grid gap-3 max-w-2xl">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Name (First and Last)</span>
|
||||
<input name="full_name" value="{{ form.full_name or '' }}" required class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Age</span>
|
||||
<input type="number" min="0" name="age" value="{{ form.age or '' }}" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Birthday</span>
|
||||
<input type="date" name="birthday" value="{{ form.birthday or '' }}" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">List of Hobbies</span>
|
||||
<textarea name="hobbies" rows="3" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700" placeholder="e.g., fishing, cooking, gaming">{{ form.hobbies or '' }}</textarea>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Favorite Gift card</span>
|
||||
<input name="gift_card" value="{{ form.gift_card or '' }}" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
<label class="grid gap-1 text-sm">
|
||||
<span class="text-slate-300">Favorite Type of movie</span>
|
||||
<input name="fav_movie" value="{{ form.fav_movie or '' }}" class="px-3 py-2 rounded-lg bg-slate-950 border border-slate-700">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="inline-flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="jewelry" value="yes" {% if form.jewelry %}checked{% endif %} class="rounded border-slate-700 bg-slate-950">
|
||||
<span class="text-slate-300">Jewelry (Yes/No)</span>
|
||||
</label>
|
||||
|
||||
<div class="pt-2">
|
||||
<button class="px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 font-semibold" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user