Commit Graph

138 Commits

Author SHA1 Message Date
1be9959481 Serve club logos directly from Caddy instead of Django
Every image request was round-tripping through a gunicorn worker
for what is just a static file on disk. Caddy now serves /media/*
straight off the shared media_data volume (mounted read-only) and
only falls through to Django for anything else — Django's own
/media/* route stays as a fallback for compose.behind-proxy.yaml
and runserver, where there is no bundled Caddy container.
2026-08-06 22:18:27 +02:00
30be424985 Serve /media/* in production without the static() DEBUG gate
django.conf.urls.static.static() hard-codes its own
`if not settings.DEBUG: return []` internally, so the earlier
AWS_STORAGE_BUCKET_NAME guard around the call never mattered — no
route was ever added outside DEBUG, and every logo still 404d.
Build the pattern directly against django.views.static.serve, which
has no such gate.
2026-08-06 22:15:16 +02:00
f6d5d18f0d Fix media volume permissions for non-root container user
/app/media didn't exist in the image, so the media_data volume had
nothing to copy ownership from on first mount — Docker created the
mount point owned by root, and the container runs as rosterchief.
Uploads then failed with PermissionError. Create the directory
before the chown so it carries the right ownership into the volume.
2026-08-06 21:58:09 +02:00
d4084de3c1 Log django.request errors to console in production
Django's default LOGGING gates its console handler behind
require_debug_true, so with DEBUG=False an unhandled exception
becomes a 500 with no trace anywhere in `docker compose logs`.
2026-08-06 21:51:17 +02:00
4d0f74379d Merge remote-tracking branch 'github/development' into development 2026-08-06 20:45:25 +02:00
5b8ab72982 Fix club logo 404 in production and persist uploads
/media/* was only routed when DEBUG=True, so uploaded club logos
404d in production regardless of storage backend. Route it whenever
local-disk storage is in use instead, and give web a persistent
volume for MEDIA_ROOT so uploads survive a rebuild.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 20:41:33 +02:00
80c1fa1226 Merge branch 'main' into development 2026-08-06 19:55:42 +02:00
30b464eb56 Update .idea module configuration and simplify CSS by removing unused classes
- Updated `.idea/rosterchief.iml` to reflect module name changes and added `.venv` exclusions.
- Cleaned up `app.css` by removing unused classes and properties from legacy styles.
2026-08-06 19:51:42 +02:00
98b8002a04 Add billing-ending banner, events CRUD, RBIHF import, public API, team photos, and sponsors
A large batch of club-management features built up over one session:

- Club dashboard banner warning admins 1 month before billing ends
- Full Events/EventSeries CRUD (recurrence builder, occurrence lifecycle,
  per-team permissions), with match->game rename and game-specific fields
  (score, competition, live status, external game ID)
- Django-admin competition dropdown, gated per-club by feature flag
- Auto-import of RBIHF fixtures (scrape -> diff -> preview -> confirm),
  with location/opponent dropdowns suggested from existing club data
- Feature-flag-gated Shop/Forms nav sections, reusing the same flag
  machinery for the RBIHF import button
- Team roster now scoped to members active this season or next, sorted and
  grouped by position
- Club sport type (ice hockey / other), shown in the control panel's club
  subtitle
- Per-season team photo upload from the team page
- New public read-only API (Django Ninja) at /api/v1/: news, team rosters,
  upcoming/live/per-team games, and sponsors -- auto-documented via Swagger
  UI, CORS-enabled for a club's own external website
- Club sponsors: admin-only CRUD (logo, URL, active date window) plus a
  date-windowed, optionally randomized API endpoint
- Assorted fixes: NullBooleanField dropdown rendering, cross-club event
  validation timing, searchable-select chip placement, btn-neutral ->
  default button style sweep, calendar-month chart windows

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
2026-08-06 17:36:04 +02:00
6ad0d6658c Add trial subscriptions with automatic switch to a pre-selected plan
A club with no subscription yet can be started on a short trial (e.g.
2 months) from the control panel, on a tier picked up front for what
it switches to once the trial ends -- no manual follow-up needed. The
trial is a real billed period on a dedicated trial tier, reusing the
existing invoice/grace/archive machinery unchanged; the switch happens
in open_period() itself so it fires whether reached via the scheduled
renewal command or a platform admin's manual "Open period" click.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
2026-08-04 12:28:47 +02:00
68cad0c951 Add a home-location box to the club detail page in the control panel
Setting a club's home ground here creates/updates the same events.Location
row (flagged is_home) that the club's own Teams > Locations page shows and
edits -- no separate sync step, it's the same record either way. Also
fixes the shared form_field templatetag: django-countries' CountryField
widget reports as "lazyselect", which fell through to a broken plain
text input instead of rendering as a dropdown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
2026-08-04 12:27:48 +02:00
e6850232f0 Rework Teams/Positions access control and add full Locations/Opponents CRUD
Team managers/coaches now only see their own teams, can't create teams,
and can view (but not edit) positions -- admins keep full rights.
Locations and Opponents move from read-only stubs to full CRUD, gated
to admins and management-position staff, with a country dropdown
(django-countries) instead of free text. Also: the team list shows
player/staff counts, and deleting a news item's main photo promotes
another one instead of leaving the item without one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
2026-08-04 12:27:24 +02:00
9a4da9b136 Add a team attendance KPI panel and no-show check-in tracking
Team pages now show, for the selected season: overall attendance rate,
best/worst attenders, players who missed the last 2 practices, and
no-shows (an affirmative RSVP checked in as absent). No-shows need a
real distinction the RSVP status alone can't make, so Attendance gains
a separate showed_up tri-state field, usable today via Django admin --
a full check-in screen is future work for the coaches app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
2026-08-03 23:08:47 +02:00
ab1d34cd0a Fix a 500 when two roster entries clash on jersey number
team/season aren't TeamMembershipForm fields (the view sets them from
the URL, not user input), so Django's own validate_unique() silently
excludes both of them -- and with them, the whole
unique_jersey_number_per_team_per_season constraint. A clashing jersey
number reached the database unrejected and came back as a raw
IntegrityError. form.clean() now checks it by hand, same as
PositionForm already does for its own check constraint; the four
roster/staff save paths also get a try/except IntegrityError backstop
so any future gap degrades to an error banner instead of a 500.
2026-08-03 22:42:19 +02:00
054e1cb1be Reorganize the nav: drop "Club setup" as its own section
Positions moves under Teams, Roles under People (below Memberships) --
matches how Members/Teams already group what they own instead of every
admin-only page getting its own top-level section.
2026-08-03 22:40:14 +02:00
18e3b0306d Redesign the management home page's KPIs and charts
Drop renewal rate from the top KPI bar; split "Upcoming events" in half
and add a "News" overview beside it (published items only, everyone
can see it, same as events). Signups-per-month, club fee status, and
renewal rate (now a pie chart matching the fee status one, not a bare
stat box) move into their own row and become admin-only like the fee
chart already was. The bottom stat-group row now sizes its columns to
how many groups actually render, instead of leaving a blank slot where
Shop would be for non-admins.
2026-08-03 22:40:06 +02:00
92686b0755 Add a branded 403 page instead of Django's bare error page
templates/403.html is picked up automatically by Django's default
permission_denied handler -- no urls.py wiring needed. Rendered through
the tenant's own skin (base_template, same as maintenance.html), so a
permission error still looks like the app and the navbar (sign out,
theme toggle, home link) stays reachable instead of leaving the user
stuck on a dead end.
2026-08-03 22:15:52 +02:00
a3e413904f Fold Roster and Staff into Teams, with a per-season switcher and real CRUD
One "Teams" nav entry now, matching how Family has none of its own and
is only reached through Members -- Roster/Staff were pure club-wide
list stubs with no way to add/remove anyone except Django admin.
Opening a team shows its roster and staff for whichever season is
selected (?season=, defaulting to the current one, same pattern as the
Memberships page); adding/editing/removing either is gated to that
team's own manager or a club admin (TeamManagerRequiredMixin, defined
earlier but never actually wired up until now).
2026-08-03 22:08:35 +02:00
9a2a06180f Add edit/delete actions to the teams list
Admin-only, matching TeamCreateView/TeamUpdateView's existing gate.
Deleting cascades away the team's roster and staff assignments -- no
ProtectedError to catch, unlike a Member that orders/invoices can still
reference. Edit links to the detail page rather than the edit form
directly, same convention as the news and member lists.
2026-08-03 21:42:23 +02:00
2261f86596 Add edit/delete actions to the news list and detail pages
Both gated the same way editing already was: broad while a draft,
editor/admin-only once published. The list's Edit link goes to the
detail page rather than straight to the edit form, matching the
member list's convention.
2026-08-03 21:38:47 +02:00
859e3e5f84 Fix file uploads in modal forms: multi-file input rendering + enctype
Two bugs, both invisible until the news photo upload modal exercised
them: the shared field template didn't recognise a custom multi-file
widget (it fell through to a plain, useless text input), and the modal
form itself had no enctype="multipart/form-data" -- so even a correctly
rendered file input would have posted filenames only, never content.
2026-08-03 21:37:04 +02:00
d9b4337319 Add a shared searchable multiselect combobox for team/member pickers
Extracts the "Grant role" member picker's typeahead combobox into a
reusable static/js/searchable-select.js (opt in via data-searchable on
the widget), and reuses it for the news form's teams field -- a proper
multiselect with removable pills and filter-as-you-type, replacing the
plain checkbox list. Needed forwarding widget attrs through the shared
select template, which never passed them to the rendered <select>.
2026-08-03 21:36:35 +02:00
38947196c1 Group the club edit form's fields into labelled sections
Branding (logo, colours) and season settings now sit in their own
divider-separated groups instead of one flat grid, so the form reads
as distinct settings groups rather than an undifferentiated field list.
2026-08-03 18:47:09 +02:00
3e0c63ec36 Add a news app: coach_manager authoring, team tagging, photos, editor release flow
News and NewsPhoto (team-tagged instead of categorised, one photo taggable
as main via a partial unique constraint), gated per club/services/access.py:
any current-season coach_manager, EDITOR, or ADMIN can draft and edit a news
item; only EDITOR/ADMIN can publish it, or edit it once it's live. Publishing
takes a date so it can be scheduled ahead of time rather than only right now.

Authoring/release only for now -- no member-facing reading page or public API
yet, the visibility field (internal/external/both) is there for when those land.
2026-08-03 18:46:37 +02:00
ce35348b31 Build out Positions CRUD and rework the Roles page
Positions had list/create/edit already stubbed as list-only; give it real
forms, with a check mirroring the management_position_implies_staff_position
constraint so a bad combination reads as a form error, not a 500. Roles now
groups by role (excluding the MEMBER everyone holds automatically, which was
just noise), grants via a modal instead of a separate page, and its member
picker is a small typeahead combobox instead of a long native <select>.
2026-08-03 18:45:28 +02:00
9b0af5800a Give outline buttons an explicit neutral color
Every plain btn-outline (Cancel, Edit, Search, Waive payment, ...) rode
on daisyUI's default outline color, which reads as primary-tinted in
some themes -- explicit btn-neutral keeps secondary actions visually
distinct from the real primary/error actions beside them.

Also gives the club edit form's fields an explicit layout (a spacer
next to the logo upload, season settings paired with branding) instead
of a generic field loop.
2026-08-03 17:05:59 +02:00
2c4d031050 Show just surnames for an unnamed family, not "... family"
An unnamed family with distinct member surnames read as "Jones / Smith
family" -- dropping the suffix reads more like an actual family name.
2026-08-03 17:04:09 +02:00
d5f45c9404 Make user-facing strings translatable
Statistics labels, shop help text, and the confirm/form modal defaults
were plain strings; wrap them per CLAUDE.md's i18n convention so the
app stays translation-ready as it's written.
2026-08-03 17:03:49 +02:00
8598fd2b46 Generate seasons per club instead of a hardcoded Aug-May window
Club now carries its own season_start and season_duration_months,
editable via controlpanel; generate_seasons chains each new season off
the day after the club's last one ends (or its configured start, for a
club with none yet) instead of assuming every club runs Aug 1 - May 31.

Adds --resync to the generate_seasons command to clean up seasons left
over from the old hardcoded rule -- removing any that don't match a
club's current settings and aren't still referenced by real data.
2026-08-03 17:03:03 +02:00
062da00bb9 Add the management app: club-facing UI + real fee-payment tracking
Gives clubs a self-service /manage/ area for members, families, teams,
roles, and season memberships, alongside real fee-payment tracking
(FeePayment, record_payment/mark_as_paid/remaining_balance) so a
membership's paid status reflects actual money received instead of a
single manually-set flag.
2026-08-03 17:01:15 +02:00
5bea8a4a6c Keep /media/ reachable during maintenance, so the club logo still loads
The maintenance page is deliberately rendered through the club's own
skin so it can show the club's logo -- but MaintenanceMiddleware closed
every path on a club subdomain unconditionally, including /media/. The
logo's <img> then pointed at a URL that itself returned the maintenance
page (503, text/html) instead of the image, so it rendered as broken.

/media/ moves into ALWAYS_OPEN, checked before the club-vs-platform
branch, so it now stays reachable on every host during a lock-down --
matching what OPEN_PREFIXES already granted the base domain, just never
extended to a club subdomain.
2026-07-27 11:56:10 +02:00
febde41214 Let the club logo fill more of its ring, match it in the control panel
Drops the inner padding on the logo image so it fills the circle right
up to the ring, rather than floating small in the middle of it -- most
visible on logos with generous internal whitespace (e.g. some SVGs).

The control panel's club-detail page now frames the logo the same way.
It doesn't get the page-wide --color-primary override the club's own
site uses (the panel must never dress itself up as the club), so the
ring colour is set as a locally-scoped custom property on just this
element instead.
2026-07-27 11:15:08 +02:00
a86c35cbe7 Frame the club logo with a primary-coloured ring, sharpen its scaling
Wraps the uploaded logo in the same avatar shape as the initials
fallback (so the header doesn't jump in size depending on whether a
club has one) and adds a ring in the club's own primary colour as a
plain border around it.

Also hints the browser to use its higher-quality image scaler: a
club's raster logo is often much smaller than the badge it's shown in,
and the default upscaling in some engines reads as pixelated. Doesn't
affect SVG logos, which scale losslessly regardless.
2026-07-27 11:06:08 +02:00
c17e7b5059 Remove unused CSS utility classes and redundant theme variables
Cleans up unused Tailwind CSS classes, DaisyUI components, and theme variables from `static/css/app.css`. Also removes an unnecessary margin class in `field.html` for cleaner templates.
2026-07-27 10:56:30 +02:00
c1ba6e269b Rebuild compiled CSS to include bg-secondary / text-secondary-content
These are the club avatar's classes for the new secondary_color feature.
Tailwind only emits utility classes it sees used in a template scan, and
the dev watcher wasn't running when that avatar edit landed, so the
classes were silently absent from static/css/app.css: no background,
no text colour, just an empty (visually white) circle. A one-off build
regenerates them; both now correctly reference var(--color-secondary)
and var(--color-secondary-content) so a club's colour picks apply.
2026-07-27 10:37:43 +02:00
0223383c9d Fix flaky chart date windows: use calendar months, not 30-day steps
_monthly and signup_split approximated "N months ago" as N*30 days, which
drifts against real calendar months by several days a year. Late in some
months that drift undershot a full month, so the "dense 13-point series"
tests flaked depending on which day they ran (confirmed: every 28th+ of
most months). Switched to dateutil.relativedelta for exact calendar-month
arithmetic, which is stable on every day of every month.
2026-07-27 10:37:37 +02:00
90a99fb03e Rename PyCharm module to match the RosterChief project name 2026-07-27 10:27:18 +02:00
75679dcc40 Fix club-metrics test copy after the dashboard relabel
"No coach" was renamed to "Teams without coach" and the "Unpaid, by
age" card was replaced by the fee-status pie chart; the test still
asserted the old strings.
2026-07-27 10:27:13 +02:00
88a6c63f9f Allow SVG club logos
ImageField validates uploads through Pillow, which can't read SVGs, so
a club crest that's a vector logo was rejected outright. Switches to a
plain FileField with an extension allowlist (png/jpg/jpeg/gif/webp/svg)
instead, and restricts the file picker to image types via the widget's
accept attribute.
2026-07-27 10:27:07 +02:00
075c2918b6 Add a secondary brand colour for clubs
Mirrors primary_color: a club-picked hex highlight used for accents like
the avatar-initials badge, with a computed readable text colour so a
pale pick doesn't produce white-on-yellow text. The shared contrast math
moves into _content_color_for so both colours use the same rule.
2026-07-27 10:26:31 +02:00
ccaa9e991a Add a generic form_field templatetag for daisyUI form rendering
_form_fields.html and club_form.html each hand-rolled the same
label/input/help-text/error markup per field, one copy per widget type,
which drifted out of sync between the two partials. {% form_field %}
picks the right daisyUI markup from the field's widget type in one
place, so every form that renders fields this way gets the same
behaviour (size modifiers, placeholder-as-label, required badges) for
free.
2026-07-27 10:24:28 +02:00
bf1ec322ce Simplify add_one_year using dateutil.relativedelta
Replaces the hand-rolled leap-day fallback with the library that already
handles it, and already sits in the dependency tree.
2026-07-27 10:22:42 +02:00
08bf76e762 Make the OTP boxes focus the input when tapped
The overlaid OTP input has pointer-events: none so clicks land on the
decorative boxes, but that also means a tap never reached the real input
on a touchscreen (desktop got away with it via autofocus/Tab). Wrapping
the boxes in a <label for> restores focus-on-click without adding a DOM
child that would throw off the otp box count.
2026-07-27 10:20:53 +02:00
438d111718 Make the navbar responsive on small phone widths
Icon-only buttons and truncated labels below the sm breakpoint keep the
navbar from forcing the page wider than the device. Also adds a Django
admin shortcut for superusers and pins the scrollbar gutter so short and
tall pages no longer shift width against each other.
2026-07-27 10:19:15 +02:00
40255805c3 Modularize confirmation modal for destructive POST actions and add notify helper for concise message handling across the UI. v0.1.0 2026-07-16 18:34:03 +02:00
127d0e338e Modularize and streamline billing templates; replace _billing_form.html with reusable modals and shared partials, and update styles and interactions for consistency and clarity. billing, feature management, and forms
Refactored club detail templates to modularize common UI components. Standardized layout, interactions, and styles across admin, billing, and feature cards for consistency and reusability.
2026-07-16 16:44:48 +02:00
83caa233d7 Replace doughnut chart with pie chart in fees visualization and clean up unused CSS styles. 2026-07-16 08:38:17 +02:00
fc7a349f8f Add "Open" button to club detail for direct access to club subdomain 2026-07-16 00:12:29 +02:00
f403128f57 Improve club detail layout: enhance logo display, adjust badge styles, refine card layouts, and optimize spacing 2026-07-16 00:06:54 +02:00
91270b0cf8 Add "Plan end" column to health table and simplify badge styles
The health table now includes a "Plan end" column showing the coverage end date ("-" if none). Simplified several badge styles for consistency, replacing "n/a" with "-" and updating waived badges to use a ghost-outline class.
2026-07-15 18:28:02 +02:00