feat: add data-test attributes for improved testing in various components

This commit is contained in:
T. Zehetbauer
2026-04-01 10:23:35 +02:00
parent fd8c2cc32a
commit 3bcc5c70a3
20 changed files with 802 additions and 31 deletions

View File

@@ -0,0 +1,171 @@
{
"nav": {
"overview": "Übersicht",
"waters": "Gewässer",
"species": "Fischarten",
"stocking": "Besatz",
"leases": "Pachten",
"catchBooks": "Fangbücher",
"permits": "Erlaubnisscheine",
"competitions": "Wettbewerbe",
"statistics": "Statistiken"
},
"pages": {
"overviewTitle": "Fischerei",
"watersTitle": "Fischerei - Gewässer",
"speciesTitle": "Fischerei - Fischarten",
"stockingTitle": "Fischerei - Besatz",
"leasesTitle": "Fischerei - Pachten",
"catchBooksTitle": "Fischerei - Fangbücher",
"permitsTitle": "Fischerei - Erlaubnisscheine",
"competitionsTitle": "Fischerei - Wettbewerbe",
"statisticsTitle": "Fischerei - Statistiken"
},
"dashboard": {
"title": "Fischerei Übersicht",
"subtitle": "Gewässer, Fischarten, Besatz, Fangbücher und mehr verwalten",
"waters": "Gewässer",
"species": "Fischarten",
"activeLeases": "Aktive Pachten",
"openCatchBooks": "Offene Fangbücher",
"upcomingCompetitions": "Kommende Wettbewerbe",
"stockingCostsYtd": "Besatzkosten (lfd. Jahr)",
"recentStocking": "Letzte Besatzaktionen",
"noRecentStocking": "Noch keine Besatzaktionen vorhanden.",
"pendingCatchBooks": "Offene Fangbücher",
"noPendingCatchBooks": "Keine Fangbücher zur Prüfung ausstehend."
},
"waters": {
"searchPlaceholder": "Gewässer suchen...",
"newWater": "Neues Gewässer",
"title": "Gewässer ({count})",
"noWaters": "Keine Gewässer vorhanden",
"createFirst": "Erstellen Sie Ihr erstes Gewässer, um loszulegen.",
"shortName": "Kurzname",
"surfaceArea": "Fläche (ha)"
},
"waterForm": {
"basicData": "Grunddaten",
"name": "Name *",
"shortName": "Kurzname",
"waterType": "Gewässertyp",
"description": "Beschreibung",
"surfaceArea": "Fläche (ha)",
"length": "Länge (m)",
"width": "Breite (m)",
"avgDepth": "Durchschnittstiefe (m)",
"maxDepth": "Maximaltiefe (m)",
"geography": "Geografie",
"drainage": "Abfluss",
"location": "Lage/Standort",
"district": "Landkreis",
"latitude": "Breitengrad",
"longitude": "Längengrad",
"administration": "Verwaltung",
"lfvNumber": "LFV-Nummer",
"costSharePercent": "Kostenanteil DS (%)",
"waterUpdated": "Gewässer aktualisiert",
"waterCreated": "Gewässer erstellt",
"errorSaving": "Fehler beim Speichern",
"waterTypes": {
"still": "Stillgewässer",
"flowing": "Fließgewässer",
"pond": "Teich/Weiher",
"lake": "See",
"river": "Fluss",
"stream": "Bach",
"canal": "Kanal",
"reservoir": "Stausee"
}
},
"species": {
"searchPlaceholder": "Fischart suchen...",
"newSpecies": "Neue Fischart",
"title": "Fischarten ({count})",
"noSpecies": "Keine Fischarten vorhanden",
"createFirst": "Erstellen Sie Ihre erste Fischart.",
"latinName": "Lateinischer Name",
"localName": "Lokaler Name"
},
"speciesForm": {
"name": "Name *",
"latinName": "Lateinischer Name",
"localName": "Lokaler Name",
"protectionRules": "Schutzbestimmungen",
"minimumSize": "Schonmaß (cm)",
"closedSeasonStart": "Schonzeit Beginn (MM.TT)",
"closedSeasonEnd": "Schonzeit Ende (MM.TT)",
"datePlaceholder": "z.B. {example}",
"catchLimits": "Fangbegrenzungen",
"maxCatchPerDay": "Max. Fang/Tag",
"maxCatchPerYear": "Max. Fang/Jahr",
"speciesUpdated": "Fischart aktualisiert",
"speciesCreated": "Fischart erstellt",
"errorSaving": "Fehler beim Speichern"
},
"stocking": {
"newStocking": "Besatz eintragen",
"title": "Besatzeinträge ({count})",
"noStocking": "Keine Besatzeinträge vorhanden",
"createFirst": "Tragen Sie den ersten Besatz ein.",
"date": "Datum",
"water": "Gewässer",
"fishSpecies": "Fischart",
"quantity": "Anzahl",
"weight": "Gewicht (kg)",
"ageClass": "Altersklasse",
"cost": "Kosten (€)"
},
"stockingForm": {
"title": "Besatzdaten",
"water": "Gewässer *",
"selectWater": "— Gewässer wählen —",
"species": "Fischart *",
"selectSpecies": "— Fischart wählen —",
"date": "Besatzdatum *",
"quantity": "Anzahl (Stück) *",
"weight": "Gewicht (kg)",
"ageClass": "Altersklasse",
"cost": "Kosten (EUR)",
"remarks": "Bemerkungen",
"created": "Besatz eingetragen",
"errorSaving": "Fehler beim Speichern"
},
"catchBooks": {
"title": "Fangbücher ({count})",
"noCatchBooks": "Keine Fangbücher vorhanden",
"createFirst": "Erstellen Sie Ihr erstes Fangbuch.",
"year": "Jahr",
"allYears": "Alle Jahre",
"catchBookStatus": {
"open": "Offen",
"submitted": "Eingereicht",
"approved": "Genehmigt",
"rejected": "Abgelehnt",
"archived": "Archiviert"
}
},
"competitions": {
"title": "Wettbewerbe ({count})",
"newCompetition": "Neuer Wettbewerb",
"noCompetitions": "Keine Wettbewerbe vorhanden",
"createFirst": "Erstellen Sie Ihren ersten Wettbewerb."
},
"leases": {
"title": "Pachten",
"startDate": "Beginn",
"endDate": "Ende",
"indefinite": "unbefristet",
"cost": "Pachtkosten"
},
"permits": {
"title": "Erlaubnisscheine"
},
"common": {
"search": "Suchen",
"cancel": "Abbrechen",
"save": "Speichern",
"update": "Aktualisieren",
"create": "Erstellen"
}
}

View File

@@ -0,0 +1,168 @@
{
"nav": {
"members": "Mitglieder",
"newMember": "Neues Mitglied",
"applications": "Aufnahmeanträge",
"dues": "Beitragskategorien",
"departments": "Abteilungen",
"cards": "Mitgliedsausweise",
"import": "Import",
"statistics": "Statistiken"
},
"list": {
"searchPlaceholder": "Name, E-Mail oder Mitgliedsnr. suchen...",
"title": "Mitglieder ({count})",
"noMembers": "Keine Mitglieder gefunden",
"createFirst": "Erstellen Sie Ihr erstes Mitglied, um loszulegen.",
"newMember": "Neues Mitglied"
},
"detail": {
"personalData": "Persönliche Daten",
"firstName": "Vorname",
"lastName": "Nachname",
"dateOfBirth": "Geburtsdatum",
"gender": "Geschlecht",
"salutation": "Anrede",
"age": "{age} Jahre",
"contactData": "Kontaktdaten",
"email": "E-Mail",
"phone": "Telefon",
"mobile": "Mobil",
"address": "Adresse",
"street": "Straße",
"houseNumber": "Hausnummer",
"postalCode": "PLZ",
"city": "Ort",
"country": "Land",
"membership": "Mitgliedschaft",
"memberNumber": "Mitgliedsnr.",
"status": "Status",
"entryDate": "Eintrittsdatum",
"exitDate": "Austrittsdatum",
"exitReason": "Austrittsgrund",
"membershipYears": "{years} Jahre",
"bankData": "Bankdaten",
"iban": "IBAN",
"bic": "BIC",
"accountHolder": "Kontoinhaber",
"editMember": "Bearbeiten",
"terminateMember": "Kündigen",
"terminateConfirm": "Möchten Sie {name} wirklich kündigen?",
"terminated": "Mitglied wurde gekündigt",
"errorTerminating": "Fehler beim Kündigen",
"reactivated": "Mitglied wurde reaktiviert",
"errorReactivating": "Fehler beim Reaktivieren",
"notFound": "Mitglied nicht gefunden"
},
"form": {
"createTitle": "Neues Mitglied anlegen",
"editTitle": "Mitglied bearbeiten",
"created": "Mitglied erfolgreich erstellt",
"updated": "Mitglied aktualisiert",
"errorCreating": "Fehler beim Erstellen",
"errorUpdating": "Fehler beim Aktualisieren",
"gdprConsent": "DSGVO-Einwilligung",
"notes": "Notizen"
},
"status": {
"active": "Aktiv",
"inactive": "Inaktiv",
"pending": "Ausstehend",
"resigned": "Ausgetreten",
"excluded": "Ausgeschlossen",
"deceased": "Verstorben"
},
"applications": {
"title": "Aufnahmeanträge ({count})",
"noApplications": "Keine offenen Aufnahmeanträge",
"approve": "Genehmigen",
"reject": "Ablehnen",
"approved": "Antrag genehmigt Mitglied erstellt",
"rejected": "Antrag abgelehnt",
"errorApproving": "Fehler bei der Genehmigung",
"errorRejecting": "Fehler bei der Ablehnung",
"approveConfirm": "Antrag von {name} genehmigen?",
"rejectConfirm": "Antrag von {name} ablehnen? Bitte Grund angeben:",
"submitted": "Eingereicht"
},
"dues": {
"title": "Beitragskategorien",
"name": "Name",
"description": "Beschreibung",
"amount": "Betrag",
"interval": "Intervall",
"default": "Standard",
"monthly": "Monatlich",
"quarterly": "Vierteljährlich",
"semiannual": "Halbjährlich",
"annual": "Jährlich",
"create": "Erstellen",
"created": "Beitragskategorie erstellt",
"deleted": "Beitragskategorie gelöscht",
"errorCreating": "Fehler beim Erstellen",
"errorDeleting": "Fehler beim Löschen",
"deleteConfirm": "Beitragskategorie \"{name}\" wirklich löschen?",
"noCategories": "Keine Beitragskategorien vorhanden."
},
"mandates": {
"title": "SEPA-Mandate",
"iban": "IBAN *",
"bic": "BIC",
"accountHolder": "Kontoinhaber *",
"mandateDate": "Mandatsdatum",
"primary": "Primär",
"createMandate": "Mandat anlegen",
"revoke": "Widerrufen",
"revokeConfirm": "Mandat \"{reference}\" wirklich widerrufen?",
"created": "SEPA-Mandat erstellt",
"revoked": "Mandat widerrufen",
"errorCreating": "Fehler beim Erstellen",
"errorRevoking": "Fehler beim Widerrufen"
},
"departments": {
"title": "Abteilungen",
"noDepartments": "Keine Abteilungen vorhanden.",
"createFirst": "Erstellen Sie Ihre erste Abteilung.",
"newDepartment": "Neue Abteilung"
},
"cards": {
"title": "Mitgliedsausweise",
"memberCard": "MITGLIEDSAUSWEIS",
"memberSince": "Mitglied seit",
"validUntil": "Gültig bis",
"generate": "Ausweise generieren",
"download": "Herunterladen"
},
"import": {
"title": "Mitglieder importieren",
"selectFile": "CSV-Datei auswählen",
"mapColumns": "Spalten zuordnen",
"preview": "Vorschau",
"importing": "Wird importiert...",
"imported": "{count} Mitglieder erfolgreich importiert",
"errorImporting": "Fehler beim Import"
},
"statistics": {
"title": "Mitglieder-Statistiken",
"totalMembers": "Gesamtmitglieder",
"activeMembers": "Aktive Mitglieder",
"newThisYear": "Neue dieses Jahr",
"resignedThisYear": "Ausgetreten dieses Jahr"
},
"export": {
"csv": "CSV exportieren",
"excel": "Excel exportieren",
"memberNumber": "Mitgliedsnr.",
"firstName": "Vorname",
"lastName": "Nachname",
"email": "E-Mail",
"phone": "Telefon",
"postalCode": "PLZ",
"city": "Ort",
"status": "Status",
"entryDate": "Eintrittsdatum",
"iban": "IBAN",
"bic": "BIC",
"accountHolder": "Kontoinhaber"
}
}

View File

@@ -0,0 +1,171 @@
{
"nav": {
"overview": "Overview",
"waters": "Waters",
"species": "Fish Species",
"stocking": "Stocking",
"leases": "Leases",
"catchBooks": "Catch Books",
"permits": "Permits",
"competitions": "Competitions",
"statistics": "Statistics"
},
"pages": {
"overviewTitle": "Fisheries",
"watersTitle": "Fisheries - Waters",
"speciesTitle": "Fisheries - Fish Species",
"stockingTitle": "Fisheries - Stocking",
"leasesTitle": "Fisheries - Leases",
"catchBooksTitle": "Fisheries - Catch Books",
"permitsTitle": "Fisheries - Permits",
"competitionsTitle": "Fisheries - Competitions",
"statisticsTitle": "Fisheries - Statistics"
},
"dashboard": {
"title": "Fisheries Overview",
"subtitle": "Manage waters, fish species, stocking, catch books, and more",
"waters": "Waters",
"species": "Fish Species",
"activeLeases": "Active Leases",
"openCatchBooks": "Open Catch Books",
"upcomingCompetitions": "Upcoming Competitions",
"stockingCostsYtd": "Stocking Costs (YTD)",
"recentStocking": "Recent Stocking Activities",
"noRecentStocking": "No stocking activities yet.",
"pendingCatchBooks": "Pending Catch Books",
"noPendingCatchBooks": "No catch books pending review."
},
"waters": {
"searchPlaceholder": "Search waters...",
"newWater": "New Water",
"title": "Waters ({count})",
"noWaters": "No waters found",
"createFirst": "Create your first water body to get started.",
"shortName": "Short Name",
"surfaceArea": "Surface Area (ha)"
},
"waterForm": {
"basicData": "Basic Data",
"name": "Name *",
"shortName": "Short Name",
"waterType": "Water Type",
"description": "Description",
"surfaceArea": "Surface Area (ha)",
"length": "Length (m)",
"width": "Width (m)",
"avgDepth": "Average Depth (m)",
"maxDepth": "Maximum Depth (m)",
"geography": "Geography",
"drainage": "Drainage",
"location": "Location",
"district": "District",
"latitude": "Latitude",
"longitude": "Longitude",
"administration": "Administration",
"lfvNumber": "LFV Number",
"costSharePercent": "Cost Share (%)",
"waterUpdated": "Water updated",
"waterCreated": "Water created",
"errorSaving": "Error saving",
"waterTypes": {
"still": "Still Water",
"flowing": "Flowing Water",
"pond": "Pond",
"lake": "Lake",
"river": "River",
"stream": "Stream",
"canal": "Canal",
"reservoir": "Reservoir"
}
},
"species": {
"searchPlaceholder": "Search species...",
"newSpecies": "New Species",
"title": "Fish Species ({count})",
"noSpecies": "No fish species found",
"createFirst": "Create your first fish species.",
"latinName": "Latin Name",
"localName": "Local Name"
},
"speciesForm": {
"name": "Name *",
"latinName": "Latin Name",
"localName": "Local Name",
"protectionRules": "Protection Rules",
"minimumSize": "Minimum Size (cm)",
"closedSeasonStart": "Closed Season Start (MM.DD)",
"closedSeasonEnd": "Closed Season End (MM.DD)",
"datePlaceholder": "e.g. {example}",
"catchLimits": "Catch Limits",
"maxCatchPerDay": "Max Catch/Day",
"maxCatchPerYear": "Max Catch/Year",
"speciesUpdated": "Species updated",
"speciesCreated": "Species created",
"errorSaving": "Error saving"
},
"stocking": {
"newStocking": "Record Stocking",
"title": "Stocking Records ({count})",
"noStocking": "No stocking records found",
"createFirst": "Record your first stocking activity.",
"date": "Date",
"water": "Water",
"fishSpecies": "Fish Species",
"quantity": "Quantity",
"weight": "Weight (kg)",
"ageClass": "Age Class",
"cost": "Cost (€)"
},
"stockingForm": {
"title": "Stocking Data",
"water": "Water *",
"selectWater": "— Select water —",
"species": "Species *",
"selectSpecies": "— Select species —",
"date": "Stocking Date *",
"quantity": "Quantity (pcs) *",
"weight": "Weight (kg)",
"ageClass": "Age Class",
"cost": "Cost (EUR)",
"remarks": "Remarks",
"created": "Stocking recorded",
"errorSaving": "Error saving"
},
"catchBooks": {
"title": "Catch Books ({count})",
"noCatchBooks": "No catch books found",
"createFirst": "Create your first catch book.",
"year": "Year",
"allYears": "All Years",
"catchBookStatus": {
"open": "Open",
"submitted": "Submitted",
"approved": "Approved",
"rejected": "Rejected",
"archived": "Archived"
}
},
"competitions": {
"title": "Competitions ({count})",
"newCompetition": "New Competition",
"noCompetitions": "No competitions found",
"createFirst": "Create your first competition."
},
"leases": {
"title": "Leases",
"startDate": "Start",
"endDate": "End",
"indefinite": "indefinite",
"cost": "Lease Cost"
},
"permits": {
"title": "Permits"
},
"common": {
"search": "Search",
"cancel": "Cancel",
"save": "Save",
"update": "Update",
"create": "Create"
}
}

View File

@@ -0,0 +1,168 @@
{
"nav": {
"members": "Members",
"newMember": "New Member",
"applications": "Applications",
"dues": "Dues Categories",
"departments": "Departments",
"cards": "Member Cards",
"import": "Import",
"statistics": "Statistics"
},
"list": {
"searchPlaceholder": "Search name, email, or member no...",
"title": "Members ({count})",
"noMembers": "No members found",
"createFirst": "Create your first member to get started.",
"newMember": "New Member"
},
"detail": {
"personalData": "Personal Data",
"firstName": "First Name",
"lastName": "Last Name",
"dateOfBirth": "Date of Birth",
"gender": "Gender",
"salutation": "Salutation",
"age": "{age} years",
"contactData": "Contact Information",
"email": "Email",
"phone": "Phone",
"mobile": "Mobile",
"address": "Address",
"street": "Street",
"houseNumber": "House No.",
"postalCode": "ZIP",
"city": "City",
"country": "Country",
"membership": "Membership",
"memberNumber": "Member No.",
"status": "Status",
"entryDate": "Entry Date",
"exitDate": "Exit Date",
"exitReason": "Exit Reason",
"membershipYears": "{years} years",
"bankData": "Bank Details",
"iban": "IBAN",
"bic": "BIC",
"accountHolder": "Account Holder",
"editMember": "Edit",
"terminateMember": "Terminate",
"terminateConfirm": "Are you sure you want to terminate {name}?",
"terminated": "Member terminated",
"errorTerminating": "Error terminating member",
"reactivated": "Member reactivated",
"errorReactivating": "Error reactivating member",
"notFound": "Member not found"
},
"form": {
"createTitle": "Create New Member",
"editTitle": "Edit Member",
"created": "Member created successfully",
"updated": "Member updated",
"errorCreating": "Error creating member",
"errorUpdating": "Error updating member",
"gdprConsent": "GDPR Consent",
"notes": "Notes"
},
"status": {
"active": "Active",
"inactive": "Inactive",
"pending": "Pending",
"resigned": "Resigned",
"excluded": "Excluded",
"deceased": "Deceased"
},
"applications": {
"title": "Membership Applications ({count})",
"noApplications": "No pending applications",
"approve": "Approve",
"reject": "Reject",
"approved": "Application approved — member created",
"rejected": "Application rejected",
"errorApproving": "Error approving application",
"errorRejecting": "Error rejecting application",
"approveConfirm": "Approve application from {name}?",
"rejectConfirm": "Reject application from {name}? Please provide a reason:",
"submitted": "Submitted"
},
"dues": {
"title": "Dues Categories",
"name": "Name",
"description": "Description",
"amount": "Amount",
"interval": "Interval",
"default": "Default",
"monthly": "Monthly",
"quarterly": "Quarterly",
"semiannual": "Semi-annual",
"annual": "Annual",
"create": "Create",
"created": "Dues category created",
"deleted": "Dues category deleted",
"errorCreating": "Error creating category",
"errorDeleting": "Error deleting category",
"deleteConfirm": "Delete dues category \"{name}\"?",
"noCategories": "No dues categories found."
},
"mandates": {
"title": "SEPA Mandates",
"iban": "IBAN *",
"bic": "BIC",
"accountHolder": "Account Holder *",
"mandateDate": "Mandate Date",
"primary": "Primary",
"createMandate": "Create Mandate",
"revoke": "Revoke",
"revokeConfirm": "Revoke mandate \"{reference}\"?",
"created": "SEPA mandate created",
"revoked": "Mandate revoked",
"errorCreating": "Error creating mandate",
"errorRevoking": "Error revoking mandate"
},
"departments": {
"title": "Departments",
"noDepartments": "No departments found.",
"createFirst": "Create your first department.",
"newDepartment": "New Department"
},
"cards": {
"title": "Member Cards",
"memberCard": "MEMBER CARD",
"memberSince": "Member since",
"validUntil": "Valid until",
"generate": "Generate Cards",
"download": "Download"
},
"import": {
"title": "Import Members",
"selectFile": "Select CSV file",
"mapColumns": "Map columns",
"preview": "Preview",
"importing": "Importing...",
"imported": "{count} members imported successfully",
"errorImporting": "Error importing"
},
"statistics": {
"title": "Member Statistics",
"totalMembers": "Total Members",
"activeMembers": "Active Members",
"newThisYear": "New This Year",
"resignedThisYear": "Resigned This Year"
},
"export": {
"csv": "Export CSV",
"excel": "Export Excel",
"memberNumber": "Member No.",
"firstName": "First Name",
"lastName": "Last Name",
"email": "Email",
"phone": "Phone",
"postalCode": "ZIP",
"city": "City",
"status": "Status",
"entryDate": "Entry Date",
"iban": "IBAN",
"bic": "BIC",
"accountHolder": "Account Holder"
}
}

View File

@@ -21,6 +21,8 @@ const namespaces = [
'marketing',
'cms',
'verband',
'members',
'fischerei',
] as const;
const isDevelopment = process.env.NODE_ENV === 'development';

View File

@@ -95,6 +95,7 @@ export function CatchBooksDataTable({
<select
value={currentYear}
onChange={handleYearChange}
data-test="catchbooks-year-filter"
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
>
<option value="">Alle Jahre</option>
@@ -108,6 +109,7 @@ export function CatchBooksDataTable({
<select
value={currentStatus}
onChange={handleStatusChange}
data-test="catchbooks-status-filter"
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
>
{STATUS_OPTIONS.map((opt) => (

View File

@@ -59,7 +59,7 @@ export function CompetitionsDataTable({
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Wettbewerbe ({total})</h2>
<Link href={`/home/${account}/fischerei/competitions/new`}>
<Button size="sm">
<Button size="sm" data-test="competitions-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Wettbewerb
</Button>

View File

@@ -247,10 +247,19 @@ export function CreateSpeciesForm({
{/* Submit */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="species-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="species-submit-btn"
>
{isPending
? 'Wird gespeichert...'
: isEdit

View File

@@ -86,6 +86,7 @@ export function CreateStockingForm({
<FormControl>
<select
{...field}
data-test="stocking-water-select"
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
<option value=""> Gewässer wählen </option>
@@ -109,6 +110,7 @@ export function CreateStockingForm({
<FormControl>
<select
{...field}
data-test="stocking-species-select"
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
<option value=""> Fischart wählen </option>
@@ -253,7 +255,11 @@ export function CreateStockingForm({
<Button type="button" variant="outline" onClick={() => router.back()}>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="stocking-submit-btn"
>
{isPending ? 'Wird gespeichert...' : 'Besatz eintragen'}
</Button>
</div>

View File

@@ -435,10 +435,19 @@ export function CreateWaterForm({
{/* Submit */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="water-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="water-submit-btn"
>
{isPending
? 'Wird gespeichert...'
: isEdit

View File

@@ -1,8 +1,8 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
interface FischereiTabNavigationProps {
@@ -11,15 +11,27 @@ interface FischereiTabNavigationProps {
}
const TABS = [
{ id: 'overview', label: 'Übersicht', path: '' },
{ id: 'waters', label: 'Gewässer', path: '/waters' },
{ id: 'species', label: 'Fischarten', path: '/species' },
{ id: 'stocking', label: 'Besatz', path: '/stocking' },
{ id: 'leases', label: 'Pachten', path: '/leases' },
{ id: 'catch-books', label: 'Fangbücher', path: '/catch-books' },
{ id: 'permits', label: 'Erlaubnisscheine', path: '/permits' },
{ id: 'competitions', label: 'Wettbewerbe', path: '/competitions' },
{ id: 'statistics', label: 'Statistiken', path: '/statistics' },
{ id: 'overview', i18nKey: 'fischerei:nav.overview', path: '' },
{ id: 'waters', i18nKey: 'fischerei:nav.waters', path: '/waters' },
{ id: 'species', i18nKey: 'fischerei:nav.species', path: '/species' },
{ id: 'stocking', i18nKey: 'fischerei:nav.stocking', path: '/stocking' },
{ id: 'leases', i18nKey: 'fischerei:nav.leases', path: '/leases' },
{
id: 'catch-books',
i18nKey: 'fischerei:nav.catchBooks',
path: '/catch-books',
},
{ id: 'permits', i18nKey: 'fischerei:nav.permits', path: '/permits' },
{
id: 'competitions',
i18nKey: 'fischerei:nav.competitions',
path: '/competitions',
},
{
id: 'statistics',
i18nKey: 'fischerei:nav.statistics',
path: '/statistics',
},
] as const;
export function FischereiTabNavigation({
@@ -32,7 +44,7 @@ export function FischereiTabNavigation({
<div className="mb-6 border-b">
<nav
className="-mb-px flex space-x-1 overflow-x-auto"
aria-label="Fischerei Navigation"
aria-label="Fisheries Navigation"
>
{TABS.map((tab) => {
const isActive = tab.id === activeTab;
@@ -41,6 +53,7 @@ export function FischereiTabNavigation({
<Link
key={tab.id}
href={`${basePath}${tab.path}`}
data-test={`fischerei-tab-${tab.id}`}
className={cn(
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
isActive
@@ -48,7 +61,7 @@ export function FischereiTabNavigation({
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
)}
>
{tab.label}
<Trans i18nKey={tab.i18nKey} />
</Link>
);
})}

View File

@@ -76,15 +76,21 @@ export function SpeciesDataTable({
<Input
placeholder="Fischart suchen..."
className="w-64"
data-test="species-search-input"
{...form.register('search')}
/>
<Button type="submit" variant="outline" size="sm">
<Button
type="submit"
variant="outline"
size="sm"
data-test="species-search-btn"
>
Suchen
</Button>
</form>
<Link href={`/home/${account}/fischerei/species/new`}>
<Button size="sm">
<Button size="sm" data-test="species-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Fischart
</Button>

View File

@@ -65,7 +65,7 @@ export function StockingDataTable({
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Besatz ({total})</h2>
<Link href={`/home/${account}/fischerei/stocking/new`}>
<Button size="sm">
<Button size="sm" data-test="stocking-new-btn">
<Plus className="mr-2 h-4 w-4" />
Besatz eintragen
</Button>

View File

@@ -103,9 +103,15 @@ export function WatersDataTable({
<Input
placeholder="Gewässer suchen..."
className="w-64"
data-test="waters-search-input"
{...form.register('search')}
/>
<Button type="submit" variant="outline" size="sm">
<Button
type="submit"
variant="outline"
size="sm"
data-test="waters-search-btn"
>
Suchen
</Button>
</form>
@@ -114,6 +120,7 @@ export function WatersDataTable({
<select
value={currentType}
onChange={handleTypeChange}
data-test="waters-type-filter"
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
>
{WATER_TYPE_OPTIONS.map((opt) => (
@@ -124,7 +131,7 @@ export function WatersDataTable({
</select>
<Link href={`/home/${account}/fischerei/waters/new`}>
<Button size="sm">
<Button size="sm" data-test="waters-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neues Gewässer
</Button>

View File

@@ -155,6 +155,7 @@ export function ApplicationWorkflow({
size="sm"
variant="default"
disabled={isPending}
data-test="application-approve-btn"
onClick={() => handleApprove(appId)}
>
Genehmigen
@@ -163,6 +164,7 @@ export function ApplicationWorkflow({
size="sm"
variant="destructive"
disabled={isPending}
data-test="application-reject-btn"
onClick={() => handleReject(appId)}
>
Ablehnen

View File

@@ -145,6 +145,7 @@ export function DuesCategoryManager({
<label className="text-sm font-medium">Name *</label>
<Input
placeholder="z.B. Standardbeitrag"
data-test="dues-name-input"
{...form.register('name', { required: true })}
/>
</div>
@@ -155,6 +156,7 @@ export function DuesCategoryManager({
step="0.01"
min="0"
placeholder="0.00"
data-test="dues-amount-input"
{...form.register('amount', {
required: true,
valueAsNumber: true,
@@ -184,7 +186,12 @@ export function DuesCategoryManager({
</label>
</div>
<div className="flex items-end">
<Button type="submit" disabled={isCreating} className="w-full">
<Button
type="submit"
disabled={isCreating}
className="w-full"
data-test="dues-create-btn"
>
{isCreating ? 'Erstelle...' : 'Erstellen'}
</Button>
</div>
@@ -244,6 +251,7 @@ export function DuesCategoryManager({
size="sm"
variant="destructive"
disabled={isDeletePending}
data-test="dues-delete-btn"
onClick={() => handleDelete(catId, catName)}
>
Löschen

View File

@@ -174,6 +174,7 @@ export function MandateManager({
<label className="text-sm font-medium">IBAN *</label>
<Input
placeholder="DE89 3704 0044 0532 0130 00"
data-test="mandate-iban-input"
{...form.register('iban', { required: true })}
onChange={(e) => {
const value = e.target.value
@@ -185,12 +186,17 @@ export function MandateManager({
</div>
<div className="space-y-1">
<label className="text-sm font-medium">BIC</label>
<Input placeholder="COBADEFFXXX" {...form.register('bic')} />
<Input
placeholder="COBADEFFXXX"
data-test="mandate-bic-input"
{...form.register('bic')}
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">Kontoinhaber *</label>
<Input
placeholder="Max Mustermann"
data-test="mandate-holder-input"
{...form.register('accountHolder', { required: true })}
/>
</div>
@@ -214,7 +220,11 @@ export function MandateManager({
</select>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<Button type="submit" disabled={isCreating}>
<Button
type="submit"
disabled={isCreating}
data-test="mandate-create-btn"
>
{isCreating ? 'Erstelle...' : 'Mandat erstellen'}
</Button>
</div>
@@ -282,6 +292,7 @@ export function MandateManager({
size="sm"
variant="destructive"
disabled={isRevoking}
data-test="mandate-revoke-btn"
onClick={() => handleRevoke(mandateId, reference)}
>
Widerrufen

View File

@@ -136,6 +136,7 @@ export function MemberDetailView({
<div className="flex gap-2">
<Button
variant="outline"
data-test="member-edit-btn"
onClick={() =>
router.push(`/home/${account}/members-cms/${memberId}/edit`)
}
@@ -152,6 +153,7 @@ export function MemberDetailView({
<Button
variant="destructive"
disabled={isDeleting}
data-test="member-terminate-btn"
onClick={handleDelete}
>
{isDeleting ? 'Wird gekündigt...' : 'Kündigen'}

View File

@@ -110,9 +110,15 @@ export function MembersDataTable({
<Input
placeholder="Mitglied suchen..."
className="w-64"
data-test="members-search-input"
{...form.register('search')}
/>
<Button type="submit" variant="outline" size="sm">
<Button
type="submit"
variant="outline"
size="sm"
data-test="members-search-btn"
>
Suchen
</Button>
</form>
@@ -121,6 +127,7 @@ export function MembersDataTable({
<select
value={currentStatus}
onChange={handleStatusChange}
data-test="members-status-filter"
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
>
{STATUS_OPTIONS.map((opt) => (
@@ -132,6 +139,7 @@ export function MembersDataTable({
<Button
size="sm"
data-test="members-new-btn"
onClick={() => router.push(`/home/${account}/members-cms/new`)}
>
Neues Mitglied

View File

@@ -9,8 +9,12 @@ export function computeAge(
const birth = new Date(dateOfBirth);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const m = today.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
const monthDifference = today.getMonth() - birth.getMonth();
if (
monthDifference < 0 ||
(monthDifference === 0 && today.getDate() < birth.getDate())
)
age--;
return age;
}
@@ -21,8 +25,12 @@ export function computeMembershipYears(
const entry = new Date(entryDate);
const today = new Date();
let years = today.getFullYear() - entry.getFullYear();
const m = today.getMonth() - entry.getMonth();
if (m < 0 || (m === 0 && today.getDate() < entry.getDate())) years--;
const monthDifference = today.getMonth() - entry.getMonth();
if (
monthDifference < 0 ||
(monthDifference === 0 && today.getDate() < entry.getDate())
)
years--;
return Math.max(0, years);
}