Add referee assignment/eligibility system, team bulk-add, member Groups, and referee management dashboard with PDF export

Builds the referee workflow end to end: club-defined RefereeLevel/RefereeProfile
eligibility tied to teams, EventReferee assignment (member or external, with
fee/km payment tracking), an admin dashboard with KPI tiles, date-grouped game
tiles and range filters, and a downloadable payment form PDF modeled on the
club's existing paper document (using Club.legal_name when set). Also lands
team roster bulk-add, member mass-upload with family linking, and the
members.Group model, developed alongside this work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 21:39:19 +02:00
parent 86e28c317f
commit 309bd4d83e
54 changed files with 4574 additions and 88 deletions

View File

@@ -690,6 +690,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;
@@ -1195,6 +1215,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;
}
@@ -1263,6 +1298,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;
@@ -1530,6 +1586,105 @@
}
}
}
.range {
@layer daisyui.l1.l2.l3 {
appearance: none;
webkit-appearance: none;
--range-thumb: var(--color-base-100);
--range-thumb-size: calc(var(--size-selector, 0.25rem) * 6);
--range-progress: currentColor;
--range-fill: 1;
--range-p: 0.25rem;
--range-bg: currentColor;
@supports (color: color-mix(in lab, red, red)) {
--range-bg: color-mix(in oklab, currentColor 10%, #0000);
}
--range-fill-x: calc(
(var(--range-dir, 1) * -100cqw) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)
);
--range-fill-y: 0;
--range-fill-spread: calc(100cqw * var(--range-fill));
cursor: pointer;
overflow: hidden;
background-color: transparent;
vertical-align: middle;
width: clamp(3rem, 20rem, 100%);
--radius-selector-max: calc(
var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
);
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
border: none;
height: var(--range-thumb-size);
[dir="rtl"] & {
--range-dir: -1;
}
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}
&::-webkit-slider-runnable-track {
width: 100%;
background-color: var(--range-bg);
border-radius: var(--radius-selector);
height: calc(var(--range-thumb-size) * 0.5);
}
@media (forced-colors: active) {
&::-webkit-slider-runnable-track {
border: 1px solid;
}
}
@media (forced-colors: active) {
&::-moz-range-track {
border: 1px solid;
}
}
&::-webkit-slider-thumb {
position: relative;
box-sizing: border-box;
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
background-color: var(--range-thumb);
height: var(--range-thumb-size);
width: var(--range-thumb-size);
border: var(--range-p) solid;
appearance: none;
webkit-appearance: none;
inset-block-start: 50%;
color: var(--range-progress);
transform: translateY(-50%);
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
@supports (color: color-mix(in lab, red, red)) {
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
}
}
&::-moz-range-track {
width: 100%;
background-color: var(--range-bg);
border-radius: var(--radius-selector);
height: calc(var(--range-thumb-size) * 0.5);
}
&::-moz-range-thumb {
position: relative;
box-sizing: border-box;
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
background-color: currentColor;
height: var(--range-thumb-size);
width: var(--range-thumb-size);
border: var(--range-p) solid;
color: var(--range-progress);
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
@supports (color: color-mix(in lab, red, red)) {
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
}
}
&:disabled {
cursor: not-allowed;
opacity: 30%;
}
}
}
.indicator {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -1616,6 +1771,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;
@@ -2181,6 +2381,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;
@@ -2320,6 +2562,30 @@
.sticky {
position: sticky;
}
.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;
@@ -2351,9 +2617,53 @@
.top-1 {
top: var(--spacing);
}
.top-2 {
top: calc(var(--spacing) * 2);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
.left-1 {
left: var(--spacing);
}
.join {
display: inline-flex;
align-items: stretch;
--join-ss: 0;
--join-se: 0;
--join-es: 0;
--join-ee: 0;
--join-v: 0;
--join-h: 1;
@scope (&) {
> :where(:focus, :has(:focus)) {
z-index: 1;
}
@media (hover: hover) {
> :where(.btn:hover, :has(.btn:hover)) {
isolation: isolate;
}
}
:where(:scope > :first-child) {
--join-ss: var(--radius-field);
--join-se: calc(var(--radius-field) * var(--join-v));
--join-es: calc(var(--radius-field) * var(--join-h));
--join-ee: 0;
}
:where(:scope > :last-child) {
--join-ss: 0;
--join-se: calc(var(--radius-field) * var(--join-h));
--join-es: calc(var(--radius-field) * var(--join-v));
--join-ee: var(--radius-field);
}
:where(:scope > :only-child) {
--join-ss: var(--radius-field);
--join-se: var(--radius-field);
--join-es: var(--radius-field);
--join-ee: var(--radius-field);
}
}
}
.file-input {
@layer daisyui.l1.l2.l3 {
cursor: pointer;
@@ -2545,6 +2855,25 @@
}
}
}
.btn-active {
@layer daisyui.l1.l2 {
--btn-bg: var(--btn-color, var(--color-base-200));
color: var(--btn-fg, var(--color-base-content));
--btn-border: var(--btn-bg);
@supports (color: color-mix(in lab, red, red)) {
--btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%));
}
--btn-border-style: solid;
--btn-inset: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%));
--btn-shadow: 0 3px 2px -2px var(--btn-bg),
0 4px 3px -2px var(--btn-bg);
@supports (color: color-mix(in lab, red, red)) {
--btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000),
0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000);
}
isolation: isolate;
}
}
.stack {
@layer daisyui.l1.l2.l3 {
display: inline-grid;
@@ -2866,6 +3195,31 @@
}
}
}
.join-item {
@layer daisyui.l1.l2.l3.l4 {
> * {
--join-ss: initial;
--join-se: initial;
--join-es: initial;
--join-ee: initial;
}
}
border-style: solid;
border-width: var(--border, 1px);
border-start-start-radius: var(--join-ss);
border-start-end-radius: var(--join-se);
border-end-start-radius: var(--join-es);
border-end-end-radius: var(--join-ee);
&:not(:first-child, :disabled, [disabled], .btn-disabled) {
margin-inline-start: calc(var(--border, 1px) * -1 * var(--join-h));
margin-block-start: calc(var(--border, 1px) * -1 * var(--join-v));
}
&:is(:disabled, [disabled], .btn-disabled) {
border-width: var(--border, 1px);
border-inline-end-width: calc(var(--border, 1px) * var(--join-v));
border-block-end-width: calc(var(--border, 1px) * var(--join-h));
}
}
.modal-action {
@layer daisyui.l1.l2.l3 {
margin-top: calc(0.25rem * 6);
@@ -3164,12 +3518,23 @@
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.table {
display: table;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
.btn-circle {
@layer daisyui.l1.l2 {
border-radius: calc(infinity * 1px);
padding-inline: 0;
width: var(--size);
height: var(--size);
}
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
@@ -3186,6 +3551,9 @@
.h-72 {
height: calc(var(--spacing) * 72);
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
@@ -3201,6 +3569,9 @@
.w-16 {
width: calc(var(--spacing) * 16);
}
.w-20 {
width: calc(var(--spacing) * 20);
}
.w-60 {
width: calc(var(--spacing) * 60);
}
@@ -3237,18 +3608,55 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.skeleton {
@layer daisyui.l1.l2.l3 {
border-radius: var(--radius-box);
background-color: var(--color-base-300);
@media (prefers-reduced-motion: reduce) {
transition-duration: 15s;
}
will-change: background-position;
background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% );
background-size: 200% auto;
background-position-x: -50%;
@media (prefers-reduced-motion: no-preference) {
animation: skeleton 1.8s ease-in-out infinite;
}
}
}
.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);
}
@@ -3324,6 +3732,9 @@
.justify-start {
justify-content: flex-start;
}
.justify-items-start {
justify-items: start;
}
.gap-1 {
gap: var(--spacing);
}
@@ -3575,6 +3986,9 @@
--btn-shadow: 0 0 0 0 oklch(0% 0 0/0);
}
}
.mask-repeat {
mask-repeat: repeat;
}
.object-contain {
object-fit: contain;
}
@@ -3838,6 +4252,9 @@
text-decoration-line: none;
}
}
.underline {
text-decoration-line: underline;
}
.opacity-40 {
opacity: 40%;
}
@@ -3894,6 +4311,19 @@
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
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;
@@ -3905,6 +4335,17 @@
}
}
}
.input-sm {
@layer daisyui.l1.l2 {
--in-size-mul: 8;
--font-size-min: 0.75rem;
--spin-my: -2;
.floating-label:has(&) {
--top-mul: 4;
--font-size: 0.75rem;
}
}
}
.select-sm {
@layer daisyui.l1.l2 {
--sl-size-mul: 8;
@@ -4047,6 +4488,16 @@
}
}
}
.hover\:badge-neutral {
&:hover {
@media (hover: hover) {
@layer daisyui.l1.l2 {
--badge-color: var(--color-neutral);
--badge-fg: var(--color-neutral-content);
}
}
}
}
.sm\:col-span-2 {
@media (width >= 40rem) {
grid-column: span 2 / span 2;
@@ -4122,6 +4573,11 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.lg\:grid-cols-4 {
@media (width >= 64rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.lg\:grid-cols-6 {
@media (width >= 64rem) {
grid-template-columns: repeat(6, minmax(0, 1fr));
@@ -4552,6 +5008,26 @@
opacity: 0;
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
@@ -4718,6 +5194,10 @@
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@keyframes pulse {
50% {
opacity: 0.5;
@@ -4726,6 +5206,11 @@
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-space-y-reverse: 0;
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
@@ -4764,6 +5249,7 @@
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
--tw-ease: initial;
}
}
}