Large uncommitted body of work accumulated across sessions on this branch -- committing as a checkpoint so it's tracked and future worktree-isolated agents see the real codebase instead of a stale ancestor commit. Covers the management app's dedicated Tailwind theme and templates, the club onboarding requirement/signup workflow (club/services/onboarding.py, requirement/status models, sign-up dashboard), fee/status auto-activation decoupling, referee management, and the new events calendar grid service layer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
158 lines
10 KiB
HTML
158 lines
10 KiB
HTML
{% load lucide ui %}
|
|
|
|
{% comment %}
|
|
What the platform bills this club: plan, periods, and the modals for changing plan,
|
|
opening a period, and recording a payment. Included with `club`, `subscription`,
|
|
`dues`, `today`, `subscription_form`, `open_period_form`, `open_period_blurb` already
|
|
in context.
|
|
{% endcomment %}
|
|
<div class="card flex flex-col">
|
|
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-3">
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">Plan & billing</span>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('subscription_modal').showModal()">
|
|
{% lucide "layers" size=14 %} {% if subscription %}Change plan{% else %}Start billing{% endif %}
|
|
</button>
|
|
{% if not subscription %}
|
|
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('trial_modal').showModal()">
|
|
{% lucide "hourglass" size=14 %} Start trial
|
|
</button>
|
|
{% endif %}
|
|
{% if subscription %}
|
|
<button class="btn btn-primary btn-sm gap-2" type="button" onclick="document.getElementById('open_period_modal').showModal()">
|
|
{% lucide "calendar-plus" size=14 %} {% if club.is_archived %}Reactivate{% else %}Open period{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 px-4 py-3">
|
|
{% comment %}
|
|
Paying up does not un-archive a club on its own -- restoring is a deliberate act,
|
|
because a club can also be archived by hand for reasons that have nothing to do
|
|
with money. This is the prompt that makes the deliberate act one click away
|
|
instead of something you have to remember to go and check.
|
|
{% endcomment %}
|
|
{% if club.is_archived and dues_settled %}
|
|
<div class="alert alert-success">
|
|
{% lucide "circle-check" size=16 %}
|
|
<span>This club is archived but owes nothing. Reactivating will restore access and open its next period.</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not subscription %}
|
|
<p class="text-sm text-muted">This club is not billed for anything. Put it on a plan to start.</p>
|
|
{% else %}
|
|
<p class="text-sm text-slate">
|
|
On plan <strong class="text-ink">{{ subscription.plan.name }}</strong>.
|
|
{% if subscription.trial_ends_at %}
|
|
<span class="badge badge-info badge-sm gap-1">{% lucide "hourglass" size=12 %} Trial</span>
|
|
On trial until {{ subscription.trial_ends_at|date:"j M Y" }}, then switches to <strong class="text-ink">{{ subscription.post_trial_plan.name }}</strong>.
|
|
{% endif %}
|
|
{{ subscription.plan.duration_months }}-month periods, archived {{ subscription.plan.grace_days }} days after a period starts if unpaid.
|
|
{% if subscription.auto_renew %}
|
|
Renews automatically {{ subscription.plan.renewal_lead_days }} days before the period ends.
|
|
{% else %}
|
|
<span class="badge badge-warning badge-sm">Auto-renew off</span> — you must open each period by hand, or this club uses the platform for free.
|
|
{% endif %}
|
|
{% if subscription.auto_archive %}
|
|
Archived automatically when a period goes unpaid past its grace period.
|
|
{% else %}
|
|
<span class="badge badge-warning badge-sm">Auto-archive off</span> — it will never be archived for non-payment.
|
|
{% endif %}
|
|
</p>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Period</th>
|
|
<th class="text-right">Billed</th>
|
|
<th class="text-right">Paid</th>
|
|
<th class="text-right">Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for due in dues %}
|
|
<tr>
|
|
<td class="min-w-[220px] whitespace-nowrap">
|
|
{{ due.period_start|date:"j M Y" }} — {{ due.period_end|date:"j M Y" }}
|
|
<div class="font-mono text-[11px] text-muted">
|
|
{{ due.plan.name }} · {{ due.invoice.number }}
|
|
<div>grace to {{ due.grace_until|date:"j M Y" }}</div>
|
|
</div>
|
|
</td>
|
|
<td class="text-right font-mono tabular-nums">€{{ due.amount|floatformat:2 }}</td>
|
|
<td class="text-right font-mono tabular-nums">€{{ due.amount_paid|floatformat:2 }}</td>
|
|
<td class="text-right">
|
|
{% if due.status == "paid" %}
|
|
<span class="badge badge-success gap-1">{% lucide "check" size=12 %} Paid</span>
|
|
{% elif due.status == "waived" %}
|
|
<span class="badge badge-ghost gap-1">{% lucide "check" size=12 %} Waived</span>
|
|
{% elif due.grace_until < today %}
|
|
<span class="badge badge-error gap-1">{% lucide "triangle-alert" size=12 %} Overdue</span>
|
|
{% elif due.period_end < today %}
|
|
<span class="badge badge-warning gap-1">{% lucide "hourglass" size=12 %} In grace</span>
|
|
{% else %}
|
|
<span class="badge badge-ghost">{{ due.get_status_display }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="flex flex-row flex-wrap items-center justify-end gap-2">
|
|
{% if due.is_owing %}
|
|
<button class="btn btn-outline btn-sm gap-1" type="button" onclick="document.getElementById('{{ due.pk|dom_id:"due_pay_modal" }}').showModal()">{% lucide "banknote" size=14 %} Add payment</button>
|
|
{% if not due.payments.all %}
|
|
<form class="inline" method="post" action="{% url 'controlpanel:due_waive' due.pk %}">
|
|
{% csrf_token %}
|
|
<button class="btn btn-outline btn-sm gap-1" type="submit">{% lucide "ban" size=14 %} Waive payment</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
<a class="btn btn-outline btn-sm gap-1" href="{% url 'controlpanel:due_invoice' due.pk %}">{% lucide "file-down" size=14 %} Download invoice</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% for payment in due.payments.all %}
|
|
<tr class="font-mono text-[11px] text-muted">
|
|
<td colspan="2" class="pl-8">
|
|
{% lucide "corner-down-right" size=12 %}
|
|
{{ payment.paid_at|date:"j M Y" }} · {{ payment.get_method_display }}{% if payment.reference %} · {{ payment.reference }}{% endif %}
|
|
</td>
|
|
<td class="text-right tabular-nums">€{{ payment.amount|floatformat:2 }}</td>
|
|
<td colspan="2"></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted">No periods billed yet.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% comment %} Dialogs live outside the table: <tbody> may only contain <tr> elements. {% endcomment %}
|
|
{% for due in dues %}
|
|
{% if due.is_owing %}
|
|
{% url 'controlpanel:due_pay' due.pk as due_pay_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id=due.pk|dom_id:"due_pay_modal" title="Record payment" form=due.payment_form action_url=due_pay_url submit_label="Record payment" submit_icon="banknote" %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% url 'controlpanel:club_subscribe' club.pk as subscribe_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id="subscription_modal" title=subscription|yesno:"Change plan,Start billing" form=subscription_form action_url=subscribe_url submit_label="Save plan" submit_icon="layers" blurb="Changing plan does not re-bill: the current period keeps the amount it was issued at, and the new rate applies from the next one." %}
|
|
|
|
{% if not subscription %}
|
|
{% url 'controlpanel:club_trial_start' club.pk as trial_start_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id="trial_modal" title="Start trial" form=trial_form action_url=trial_start_url submit_label="Start trial" submit_icon="hourglass" blurb="The trial period is billed like any other, on the trial plan you pick. It switches to the plan you pick here automatically the next time a period is opened after it ends -- no follow-up needed." %}
|
|
{% endif %}
|
|
|
|
{% if subscription %}
|
|
{% url 'controlpanel:club_open_period' club.pk as open_period_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id="open_period_modal" title=club.is_archived|yesno:"Reactivate,Open period" form=open_period_form action_url=open_period_url submit_label="Open period" submit_icon="calendar-plus" blurb=open_period_blurb %}
|
|
{% endif %}
|