Disclaimer: dit project is gebouwd met behulp van AI (Claude Code).
Een Electron-desktopapp voor het automatisch indelen van schutters over doelen bij boogschietwedstrijden. Gemaakt voor de organisatie van wedstrijden waar schutters van verschillende gilden samen schieten op verschillende afstanden (25m, 18m, 12m), met regels rond gildediversiteit, dubbel-schutters, compound- doelen en aanmeldvolgorde.
- Schuttersbeheer: schutters opslaan met gilde, boogtype, afstand, leeftijdscategorie en geslacht. Filteren op boogtype + afstand, zoeken op naam of gilde. Inline bewerken én verwijderen vanuit de tabel.
- Categorie-regels: het formulier handhaaft automatisch dat Jeugd-schutters op 12m of 18m staan, Aspirant/Junior/Senior/Veteraan op 25m, en dat Compound-schutters geen Veteraan kunnen zijn. Ongeldige combinaties worden ter plaatse gecorrigeerd of geblokkeerd.
- CSV-import met review: bij import wordt een controle-modal getoond met alle rijen, ongeacht of er conflicten zijn. Conflicten staan bovenaan en worden inline gecorrigeerd; pas wanneer alle rijen geldig zijn kan worden geïmporteerd. UTF-8 met BOM voor Excel-compatibiliteit. Onbekende gilden worden bij commit automatisch aangemaakt.
- CSV-export van het volledige ledenbestand met dezelfde header — een export is direct weer importeerbaar.
- Bulk-acties: demo-data laden (100 voorbeeld-schutters), Gevarenzone-knop om alle schutters in één keer te wissen (cascadeert naar inschrijvingen en doelindelingen in één transactie; gilden en wedstrijden blijven behouden), en een aparte knop om alleen verlaten gilden (zonder schutters) op te ruimen.
- Wedstrijdbeheer: wedstrijden aanmaken, configureren (datum, locatie, aantal doelen per zone, compound-startdoel) en verwijderen via de Configuratie-tab.
- Inschrijvingen: schutters inschrijven via een zoekbalk in een sticky zijbalk, dubbel-schieten aanduiden per helft, nieuwe schutter aanmaken in een modal vanuit dezelfde flow.
- Automatische doelindeling: één-klik indeling die rekening houdt met alle regels (zone, gilde-diversiteit, dubbelschutters, registratievolgorde). Gebruikt het paren-gebaseerde algoritme met tweesporen-toewijzing, zie internal-docs/ALGORITHM.md.
- Handmatige aanpassing: schutters slepen tussen doelen of terug naar de aanmeldlijst, doelen individueel of allemaal tegelijk vergrendelen, indeling leegmaken.
- Conflictdetectie: gele waarschuwing per doel én een hover-samenvatting bovenaan met alle aandachtspunten over alle doelen.
- Afdrukken: per wedstrijd een live preview van de doelindelingslijst met filters (doel-interval, gilde, afstand), groepering per doel of per gilde, en native print-dialoog (incl. Microsoft Print to PDF) — zie internal-docs/AFDRUKKEN.md.
- Dark mode: volgt systeemvoorkeur bij eerste start, met handmatige toggle (zon/maan-icoon rechts in de topbar) en localStorage-persistentie.
- Electron 33 + electron-vite — desktopapp shell
- React 18 + TypeScript — UI
- CSS variables (light +
:root.dark) — theming - IBM Plex Sans / Mono — typografie
- @dnd-kit — drag-and-drop voor doelindeling
- sql.js — embedded SQLite voor lokale opslag (geen server)
npm install
npm run dev # ontwikkelmodus met hot reload
npm run build # productie-build
npm run build:win # Windows installer (.exe)target-assignment/
├── src/
│ ├── main/ # Electron main process
│ │ ├── index.ts # app entry
│ │ ├── database.ts # sql.js setup + schema
│ │ └── ipc.ts # IPC handlers (gilden, schutters,
│ │ # wedstrijden, inschrijvingen,
│ │ # indeling, demo)
│ ├── preload/ # contextBridge API + window.api types
│ └── renderer/src/ # React app
│ ├── algoritme/ # paren-gebaseerd indelingsalgoritme (v2.0)
│ │ ├── indeling.ts # hoofdalgoritme
│ │ ├── conflicten.ts
│ │ └── types.ts
│ ├── components/ # herbruikbare UI-componenten
│ │ ├── Header.tsx # topbar met brand + nav + thema-toggle
│ │ ├── SchutterKaart.tsx # draggable kaart (4-hoekenlayout)
│ │ ├── DoelKolom.tsx # doel met head, body, beurten-teller
│ │ ├── NietIngedeeldBalk.tsx
│ │ ├── SchutterFormulier.tsx # form + categorie-regel-validatie
│ │ └── ImportReviewModal.tsx # CSV-import controle met inline-edits
│ ├── pages/ # schermen
│ │ ├── WedstrijdenPage.tsx
│ │ ├── SchuttersPage.tsx
│ │ ├── WedstrijdDetailPage.tsx
│ │ ├── ConfiguratieTab.tsx
│ │ ├── InschrijvingenTab.tsx
│ │ ├── IndelingTab.tsx
│ │ └── AfdrukkenTab.tsx
│ ├── hooks/
│ │ └── useDarkMode.ts
│ ├── types.ts # gedeelde TypeScript-types
│ ├── index.css # design tokens (CSS vars) + componentstijlen
│ └── App.tsx # app-root met navigatie
├── docs/ # specificaties, regels, algoritme-documentatie
├── samples/ # voorbeeld-CSV's voor testen van import
└── electron.vite.config.ts
Alle inhoudelijke specificaties staan in docs/:
| Document | Inhoud |
|---|---|
| FEATURES.md | Functioneel overzicht van het programma |
| RULES.md | Canonieke indelingsregels: gewone taal + prioriteit (R1-R19) |
| ALGORITHM_SPEC.md | Gewenst gedrag van de indeling (input/output, randgevallen) |
| ALGORITHM.md | Werking van het algoritme (paren + tweesporen) en de doelfunctie |
| AFDRUKKEN.md | Print-systeem: opties, document-structuur, @media print-architectuur |
De volgende regels gelden in zowel het formulier als bij CSV-import. Het formulier handhaaft ze automatisch (segmented buttons disabled, dropdowns gefilterd, auto-correctie). Bij import worden conflict-rijen in de controle-modal getoond.
| Categorie | Toegestane afstand | Beperkingen |
|---|---|---|
| Aspirant | 25m | — |
| Jeugd | 12m of 18m | — |
| Junior | 25m | — |
| Senior | 25m | — |
| Veteraan | 25m | geen Compound |
De pure validator zit in
ImportReviewModal.tsx
(valideerImportRij) en wordt geïmporteerd door de form-helpers
afstandToegestaan / categorieToegestaan uit
SchutterFormulier.tsx.
Het geslacht wordt intern als M / V bewaard maar in de UI altijd via één
centrale helper getoond (categorieLabel in
src/renderer/src/lib/labels.ts). Geslacht
wordt enkelvoud weergegeven (Heer / Dame) en gecombineerd met de
leeftijdscategorie volgens onderstaande regels:
| Categorie | Heer | Dame |
|---|---|---|
| Aspirant | Aspirant | Aspirant |
| Jeugd | Jeugd | Jeugd |
| Junior | Junior | Junior |
| Senior | Senior | Dame |
| Veteraan | Veteraan Heer | Veteraan Dame |
Override voor Compound: bij type_boog = Compound wordt Senior Heer/Dame
afgekort tot enkel "Heer" / "Dame" (overige categorieën volgen de defaultregel;
Veteraan + Compound is geblokkeerd door de validatie).
Hetzelfde label wordt gebruikt in de schutterstabel, de inschrijvingenlijst en de schutterskaartjes in het indelingsvenster — voeg geen ad-hoc varianten toe, breid de helper uit als de regels wijzigen.
Voor import/export van het schuttersbestand:
voornaam,naam,gilde_naam,type_boog,leeftijdscategorie,geslacht,afstand
Jan,Janssen,Gilde De Gulden Pijl,Recurve,Senior,M,25
Marie,Peeters,Gilde De Gulden Pijl,Compound,Senior,V,25- Header is verplicht en bepaalt de kolomvolgorde.
type_boog∈{Recurve, Compound, Barebow, Andere}.leeftijdscategorie∈{Aspirant, Jeugd, Junior, Senior, Veteraan}.geslacht∈{M, V}.afstand∈{12, 18, 25}(in meter).- Onbekende gilde-namen worden automatisch aangemaakt.
- Excel-vriendelijk: export schrijft een UTF-8 BOM voor correcte weergave van accenten.
Een voorbeeld-bestand met 100 schutters waarvan 3 conflicten bevatten staat in
samples/schutters-demo.csv — handig om de
review-flow te testen.
Light-mode gebruikt een warme grijsschaal met de drie kleuren van een boogschiet-doel als accenten:
- Geel
#f5c518— primaire actie-accent (auto-indelen, dubbel-badge) - Rood
#e63946— compound-zone, gevarenzone - Blauw
#1d70b8— secundair accent (focus rings, links)
Boogtype is herkenbaar aan een 4-pixel gekleurde linkerstrip op elke schutterkaart: blauw=Recurve, rood=Compound, geel=Barebow.
Bij wijzigingen aan het indelingsalgoritme:
- Lees eerst internal-docs/RULES.md (canonieke regels + prioriteit) en internal-docs/ALGORITHM_SPEC.md (gewenst gedrag).
- Voor de huidige implementatie en de doelfunctie zie
internal-docs/ALGORITHM.md en
src/renderer/src/algoritme/indeling.ts.
Bij wijzigingen aan de UI:
- Design-tokens en alle componentstijlen staan in
src/renderer/src/index.css— gebruik de bestaande klassen (.btn,.card,.chip,.schutter,.doel,.aanmeldlijst,.split-pane,.config-card, …) en de CSS-variables uit:root. - Dark mode werkt automatisch: zet enkel CSS-variables in
:root.darkom ze in beide modi gedekt te hebben. - Componenten in
src/renderer/src/components/zijn herbruikbaar; schermen insrc/renderer/src/pages/orkestreren ze.
Bij wijzigingen aan de schutter-validatieregels:
- Centraliseer in
valideerImportRij(pure functie) — die wordt zowel bij parse-tijd, in de review-modal en bij eindcommit gebruikt. - Het formulier hergebruikt de helpers
afstandToegestaan/categorieToegestaanvoor disable/filter-logica.
Bij wijzigingen aan de database:
- Schema en migraties in
src/main/database.ts. - IPC-handlers in
src/main/ipc.ts, viawindow.apiin de renderer (ziesrc/preload/index.tsensrc/preload/index.d.ts). - Voor bulk-acties: gebruik
transaction(() => { … })zodat alles in één transactie wordt gepleegd (zieschutters:deleteAllofdemo:laad). - Aparte cleanup-IPC's voor afhankelijke tabellen:
gilden:deleteLegeverwijdert alleen gilden zonder schutters,gilden:getMetSchutterslevert de filter-lijst voor de UI.