feat: enhance accessibility and testing with data-test attributes and improve error handling
This commit is contained in:
@@ -85,11 +85,11 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Zimmer wählen —</option>
|
||||
{rooms.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
{r.roomNumber}
|
||||
{r.name ? ` – ${r.name}` : ''} ({r.pricePerNight}{' '}
|
||||
€/Nacht)
|
||||
{rooms.map((room) => (
|
||||
<option key={room.id} value={room.id}>
|
||||
{room.roomNumber}
|
||||
{room.name ? ` – ${room.name}` : ''} (
|
||||
{room.pricePerNight} €/Nacht)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -240,10 +240,19 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
data-test="booking-cancel-btn"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
data-test="booking-submit-btn"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Buchung erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -296,10 +296,19 @@ export function CreateCourseForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<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="course-cancel-btn"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="course-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Kurs erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -352,10 +352,19 @@ export function CreateEventForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<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="event-cancel-btn"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="event-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Veranstaltung erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success('Rechnung erfolgreich erstellt');
|
||||
router.push(`/home/${account}/finance-cms`);
|
||||
router.push(`/home/${account}/finance/invoices`);
|
||||
}
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
@@ -98,7 +98,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Rechnungsnummer *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input data-test="invoice-number-input" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -111,7 +111,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Rechnungsdatum</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
<Input
|
||||
type="date"
|
||||
data-test="invoice-issue-date-input"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -124,7 +128,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Fälligkeitsdatum *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
<Input
|
||||
type="date"
|
||||
data-test="invoice-due-date-input"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -145,7 +153,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Name *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input data-test="invoice-recipient-input" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -178,6 +186,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-test="invoice-add-item-btn"
|
||||
onClick={() =>
|
||||
append({ description: '', quantity: 1, unitPrice: 0 })
|
||||
}
|
||||
@@ -287,6 +296,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
min={0}
|
||||
step="0.5"
|
||||
className="max-w-[120px]"
|
||||
data-test="invoice-tax-rate-input"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
@@ -329,10 +339,19 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
data-test="invoice-cancel-btn"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
data-test="invoice-submit-btn"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Rechnung erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
data-test="sepa-batch-type-select"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="direct_debit">
|
||||
@@ -104,6 +105,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="z.B. Mitgliedsbeiträge Q1 2026"
|
||||
data-test="sepa-batch-description-input"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -119,7 +121,11 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Ausführungsdatum *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
<Input
|
||||
type="date"
|
||||
data-test="sepa-batch-date-input"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -129,10 +135,19 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
data-test="sepa-batch-cancel-btn"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
data-test="sepa-batch-submit-btn"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Einzug erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Betreff *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} data-test="newsletter-subject-input" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -88,6 +88,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
rows={12}
|
||||
className="border-input bg-background flex min-h-[200px] w-full rounded-md border px-3 py-2 font-mono text-sm"
|
||||
placeholder="<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>"
|
||||
data-test="newsletter-body-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -106,6 +107,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
rows={4}
|
||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||
placeholder="Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung"
|
||||
data-test="newsletter-text-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -127,7 +129,11 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Geplanter Versand (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="datetime-local" {...field} />
|
||||
<Input
|
||||
type="datetime-local"
|
||||
{...field}
|
||||
data-test="newsletter-schedule-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Leer lassen, um den Newsletter als Entwurf zu speichern.
|
||||
@@ -140,10 +146,19 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<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="newsletter-cancel-btn"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="newsletter-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -148,10 +148,19 @@ export function CreatePageForm({ accountId, account }: Props) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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="page-cancel-btn"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="page-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,19 @@ export function CreatePostForm({ accountId, account }: Props) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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="post-cancel-btn"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="post-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -465,15 +465,17 @@ const EventListBlock = ({
|
||||
<h2 className="mb-6 text-2xl font-bold">Veranstaltungen</h2>
|
||||
<div className="space-y-3">
|
||||
{items.map((event) => {
|
||||
const d = new Date(event.event_date);
|
||||
const eventDate = new Date(event.event_date);
|
||||
const isExpanded = expandedId === event.id;
|
||||
const isSuccess = successId === event.id;
|
||||
return (
|
||||
<div key={event.id} className="overflow-hidden rounded-lg border">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="bg-primary/10 text-primary flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg">
|
||||
<span className="text-lg font-bold">{d.getDate()}</span>
|
||||
<span className="text-xs">{formatMonthShort(d)}</span>
|
||||
<span className="text-lg font-bold">
|
||||
{eventDate.getDate()}
|
||||
</span>
|
||||
<span className="text-xs">{formatMonthShort(eventDate)}</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-semibold">{event.name}</h3>
|
||||
|
||||
@@ -83,6 +83,7 @@ export function CreateProtocolForm({
|
||||
<FormLabel>Titel *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test="protocol-title-input"
|
||||
placeholder="z.B. Vorstandssitzung März 2026"
|
||||
{...field}
|
||||
/>
|
||||
@@ -100,7 +101,11 @@ export function CreateProtocolForm({
|
||||
<FormItem>
|
||||
<FormLabel>Sitzungsdatum *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
<Input
|
||||
data-test="protocol-date-input"
|
||||
type="date"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -115,6 +120,7 @@ export function CreateProtocolForm({
|
||||
<FormLabel>Sitzungsart *</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
data-test="protocol-type-select"
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
@@ -136,7 +142,11 @@ export function CreateProtocolForm({
|
||||
<FormItem>
|
||||
<FormLabel>Ort</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="z.B. Vereinsheim" {...field} />
|
||||
<Input
|
||||
data-test="protocol-location-input"
|
||||
placeholder="z.B. Vereinsheim"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -223,10 +233,19 @@ export function CreateProtocolForm({
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
data-test="protocol-cancel-btn"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="protocol-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird gespeichert...' : 'Protokoll erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
interface MeetingsTabNavigationProps {
|
||||
@@ -10,9 +11,9 @@ interface MeetingsTabNavigationProps {
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'overview', label: 'Übersicht', path: '' },
|
||||
{ id: 'protocols', label: 'Protokolle', path: '/protocols' },
|
||||
{ id: 'tasks', label: 'Offene Aufgaben', path: '/tasks' },
|
||||
{ id: 'overview', i18nKey: 'meetings:nav.overview', path: '' },
|
||||
{ id: 'protocols', i18nKey: 'meetings:nav.protocols', path: '/protocols' },
|
||||
{ id: 'tasks', i18nKey: 'meetings:nav.tasks', path: '/tasks' },
|
||||
] as const;
|
||||
|
||||
export function MeetingsTabNavigation({
|
||||
@@ -25,7 +26,7 @@ export function MeetingsTabNavigation({
|
||||
<div className="mb-6 border-b">
|
||||
<nav
|
||||
className="-mb-px flex space-x-1 overflow-x-auto"
|
||||
aria-label="Sitzungsprotokolle Navigation"
|
||||
aria-label="Meeting Protocols Navigation"
|
||||
>
|
||||
{TABS.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
@@ -34,6 +35,7 @@ export function MeetingsTabNavigation({
|
||||
<Link
|
||||
key={tab.id}
|
||||
href={`${basePath}${tab.path}`}
|
||||
data-test={`meetings-tab-${tab.id}`}
|
||||
className={cn(
|
||||
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
|
||||
isActive
|
||||
@@ -41,7 +43,7 @@ export function MeetingsTabNavigation({
|
||||
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
<Trans i18nKey={tab.i18nKey} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -174,6 +174,7 @@ export function OpenTasksView({
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
data-test="tasks-prev-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
@@ -182,6 +183,7 @@ export function OpenTasksView({
|
||||
Zurück
|
||||
</Button>
|
||||
<Button
|
||||
data-test="tasks-next-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
|
||||
@@ -141,6 +141,7 @@ export function ProtocolItemsList({
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<Badge
|
||||
data-test="item-status-toggle"
|
||||
variant={
|
||||
(ITEM_STATUS_COLORS[item.status] as
|
||||
| 'default'
|
||||
@@ -156,6 +157,7 @@ export function ProtocolItemsList({
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button
|
||||
data-test="item-delete-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
|
||||
@@ -69,16 +69,16 @@ export function ProtocolsDataTable({
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
(formEvent: React.FormEvent) => {
|
||||
formEvent.preventDefault();
|
||||
updateParams({ q: form.getValues('search') });
|
||||
},
|
||||
[form, updateParams],
|
||||
);
|
||||
|
||||
const handleTypeChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
updateParams({ type: e.target.value });
|
||||
(changeEvent: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
updateParams({ type: changeEvent.target.value });
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
@@ -96,17 +96,24 @@ export function ProtocolsDataTable({
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<form onSubmit={handleSearch} className="flex gap-2">
|
||||
<Input
|
||||
data-test="protocols-search-input"
|
||||
placeholder="Protokoll suchen..."
|
||||
className="w-64"
|
||||
{...form.register('search')}
|
||||
/>
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-test="protocols-search-btn"
|
||||
>
|
||||
Suchen
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
data-test="protocols-type-filter"
|
||||
value={currentType}
|
||||
onChange={handleTypeChange}
|
||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||
@@ -119,7 +126,7 @@ export function ProtocolsDataTable({
|
||||
</select>
|
||||
|
||||
<Link href={`/home/${account}/meetings/protocols/new`}>
|
||||
<Button size="sm">
|
||||
<Button size="sm" data-test="protocols-new-btn">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Neues Protokoll
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user