Add a dues-invoicing feature: send, track and remind on membership fees

New DuesInvoice model (one per membership, resendable) plus
club.services.invoicing: resolves the best email to invoice (the
member's own, else a parent/guardian's), snapshots the outstanding
balance and a due date on send, and mails a branded HTML invoice
(same club-colour email shell as the parent-claim email) with a
WeasyPrint PDF attached when the native libs are available.

Dues & billing gains a bulk "Send invoice" action (checkbox selection
+ a shared due-in-days prompt), a per-row invoice status column, a
staff-facing invoice detail/PDF page, and a push-button "Send
reminders" action for every sent, unpaid invoice past its own due
date.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-20 21:23:06 +02:00
parent 44ddf7b6eb
commit 94d6cbd4e9
19 changed files with 1334 additions and 8 deletions

View File

@@ -785,6 +785,26 @@
}
}
}
.collapse-plus {
@layer daisyui.l1.l2 {
> .collapse-title:after {
position: absolute;
display: block;
height: 0.5rem;
width: 0.5rem;
@media (prefers-reduced-motion: no-preference) {
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
top: 0.9rem;
inset-inline-end: 1.4rem;
--tw-content: "+";
content: var(--tw-content);
pointer-events: none;
}
}
}
.dropdown {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -1330,6 +1350,13 @@
}
}
}
.validator-hint {
@layer daisyui.l1.l2.l3 {
visibility: hidden;
margin-top: calc(0.25rem * 2);
font-size: 0.75rem;
}
}
.validator {
@layer daisyui.l1.l2.l3 {
&:user-valid, &:has(:user-valid) {
@@ -1353,6 +1380,21 @@
}
}
}
.collapse-open {
@layer daisyui.l1.l2 {
grid-template-rows: max-content 1fr;
> .collapse-content {
--overflow-delay: 0.2s;
overflow: revert-layer;
content-visibility: visible;
min-height: fit-content;
padding-bottom: 1rem;
@supports not (content-visibility: visible) {
visibility: visible;
}
}
}
}
.collapse {
visibility: collapse;
}
@@ -1421,6 +1463,27 @@
}
}
}
.toast {
@layer daisyui.l1.l2.l3 {
position: fixed;
inset-inline-start: auto;
inset-inline-end: calc(0.25rem * 4);
top: auto;
bottom: calc(0.25rem * 4);
display: flex;
flex-direction: column;
gap: calc(0.25rem * 2);
background-color: transparent;
translate: var(--toast-x, 0) var(--toast-y, 0);
width: max-content;
max-width: calc(100vw - 2rem);
& > * {
@media (prefers-reduced-motion: no-preference) {
animation: toast 0.25s ease-out;
}
}
}
}
.toggle {
@layer daisyui.l1.l2.l3 {
border: var(--border) solid currentColor;
@@ -1873,6 +1936,51 @@
}
}
}
.aura {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-block;
--aura-padding: 0.125rem;
padding: var(--aura-padding);
border-radius: calc(var(--aura-padding) + var(--aura-radius, var(--radius-box)));
animation: aura var(--tw-duration, 6s) linear infinite;
background-image: conic-gradient(from var(--aura-angle), transparent 225deg, currentColor);
&:has( > .card, > .alert) {
--aura-radius: var(--radius-box);
}
&:has( > .btn, > .input, > .select) {
--aura-radius: var(--radius-field);
}
&:has( > .checkbox, > .toggle, > .badge) {
--aura-radius: var(--radius-selector);
}
&:before, &:after {
animation: inherit;
background-color: inherit;
background-image: inherit;
border-radius: inherit;
position: absolute;
top: calc(1 / 2 * 100%);
left: calc(1 / 2 * 100%);
z-index: 0;
display: block;
opacity: 70%;
filter: blur(0.25rem);
translate: -50% -50%;
width: 100%;
height: 100%;
content: "";
}
&:after {
opacity: 30%;
filter: blur(1rem);
}
& > * {
position: relative;
z-index: 1;
}
}
}
.steps {
@layer daisyui.l1.l2.l3 {
display: inline-grid;
@@ -2395,6 +2503,48 @@
}
}
}
.rating {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-flex;
vertical-align: middle;
--size: var(--size-selector, 0.25rem) * 6;
input {
cursor: pointer;
appearance: none;
}
* {
border-radius: 0;
background-color: var(--color-base-content);
opacity: 20%;
width: calc(var(--size) * 1);
height: calc(var(--size));
@media (prefers-reduced-motion: no-preference) {
animation: rating 0.25s ease-out;
}
}
.rating-hidden {
width: calc(0.25rem * 2);
background-color: transparent;
}
:checked, [aria-checked="true"], [aria-current="true"], :has( ~ :checked, ~ [aria-checked="true"], ~ [aria-current="true"]) {
opacity: 100%;
}
:focus-visible {
scale: 1.1;
@media (prefers-reduced-motion: no-preference) {
transition: scale 0.2s ease-out;
}
}
:active:focus {
animation: none;
scale: 1.1;
}
}
@layer daisyui.l1.l2 {
--size: var(--size-selector, 0.25rem) * 6;
}
}
.navbar {
@layer daisyui.l1.l2.l3 {
display: flex;
@@ -2545,6 +2695,30 @@
.inset-0 {
inset: 0;
}
.dropdown-right {
@layer daisyui.l1.l2 {
--anchor-h: right;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-start: 100%;
top: 0;
bottom: auto;
transform-origin: 0;
}
}
}
.dropdown-left {
@layer daisyui.l1.l2 {
--anchor-h: left;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-end: 100%;
top: 0;
bottom: auto;
transform-origin: 100%;
}
}
}
.dropdown-end {
@layer daisyui.l1.l2 {
--anchor-h: span-left;
@@ -3041,6 +3215,26 @@
.z-50 {
z-index: 50;
}
.tab-content {
@layer daisyui.l1.l2.l3 {
order: var(--tabcontent-order);
display: none;
border-color: transparent;
--tabcontent-radius-ss: var(--radius-box);
--tabcontent-radius-se: var(--radius-box);
--tabcontent-radius-es: var(--radius-box);
--tabcontent-radius-ee: var(--radius-box);
--tabcontent-order: 1;
width: 100%;
height: calc(100% - var(--tab-height) + var(--border));
margin: var(--tabcontent-margin);
border-width: var(--border);
border-start-start-radius: var(--tabcontent-radius-ss);
border-start-end-radius: var(--tabcontent-radius-se);
border-end-start-radius: var(--tabcontent-radius-es);
border-end-end-radius: var(--tabcontent-radius-ee);
}
}
.col-span-2 {
grid-column: span 2 / span 2;
}
@@ -3443,6 +3637,20 @@
}
}
}
.fieldset-label {
@layer daisyui.l1.l2.l3 {
display: flex;
align-items: center;
gap: calc(0.25rem * 1.5);
color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
}
&:has(input) {
cursor: pointer;
}
}
}
.alert {
border-width: var(--border);
border-color: var(--alert-border-color, var(--color-base-200));
@@ -3590,6 +3798,9 @@
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.table {
display: table;
}
@@ -3867,6 +4078,9 @@
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
}
@@ -3900,6 +4114,18 @@
}
}
}
.aura-glow {
@layer daisyui.l1.l2 {
animation: none;
background-image: radial-gradient(closest-corner at center, currentColor 0%, transparent 90%);
&:before {
animation: aura-glow var(--tw-duration, 6s) ease-out infinite;
}
&:after {
animation: aura-glow-after var(--tw-duration, 6s) ease-out infinite;
}
}
}
.animate-pulse {
animation: var(--animate-pulse);
}
@@ -4288,6 +4514,9 @@
--btn-shadow: 0 0 0 0 oklch(0% 0 0/0);
}
}
.mask-repeat {
mask-repeat: repeat;
}
.stroke-2 {
stroke-width: 2;
}
@@ -4788,6 +5017,9 @@
text-decoration-line: none;
}
}
.underline {
text-decoration-line: underline;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -4886,6 +5118,14 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.input-lg {
@layer daisyui.l1.l2 {
--in-size-mul: 12;
@@ -5938,6 +6178,10 @@
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@keyframes pulse {
50% {
opacity: 0.5;
@@ -6002,6 +6246,7 @@
--tw-backdrop-opacity: initial;
--tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial;
--tw-ease: initial;
}
}
}