Commits all remaining uncommitted local work: - apps/web: fischerei, verband, modules, members-cms, documents, newsletter, meetings, site-builder, courses, bookings, events, finance pages and components - apps/web: marketing page updates, layout, paths config, next.config.mjs, styles/makerkit.css - apps/web/i18n: documents, fischerei, marketing, verband (de+en) - packages/features: finance, fischerei, member-management, module-builder, newsletter, sitzungsprotokolle, verbandsverwaltung server APIs and components - packages/ui: button.tsx updates - pnpm-lock.yaml
195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
'use client';
|
||
|
||
import { useCallback } from 'react';
|
||
|
||
import { useRouter } from 'next/navigation';
|
||
|
||
import { useAction } from 'next-safe-action/hooks';
|
||
|
||
import { formatDate } from '@kit/shared/dates';
|
||
import { Badge } from '@kit/ui/badge';
|
||
import { Button } from '@kit/ui/button';
|
||
import { toast } from '@kit/ui/sonner';
|
||
|
||
import {
|
||
APPLICATION_STATUS_VARIANT,
|
||
APPLICATION_STATUS_LABEL,
|
||
} from '../lib/member-utils';
|
||
import {
|
||
approveApplication,
|
||
rejectApplication,
|
||
} from '../server/actions/member-actions';
|
||
|
||
interface ApplicationWorkflowProps {
|
||
applications: Array<Record<string, unknown>>;
|
||
accountId: string;
|
||
account: string;
|
||
}
|
||
|
||
export function ApplicationWorkflow({
|
||
applications,
|
||
accountId,
|
||
account,
|
||
}: ApplicationWorkflowProps) {
|
||
const router = useRouter();
|
||
|
||
const { execute: executeApprove, isPending: isApproving } = useAction(
|
||
approveApplication,
|
||
{
|
||
onSuccess: ({ data }) => {
|
||
if (data?.success) {
|
||
toast.success('Antrag genehmigt – Mitglied wurde erstellt');
|
||
router.refresh();
|
||
}
|
||
},
|
||
onError: ({ error }) => {
|
||
toast.error(error.serverError ?? 'Fehler beim Genehmigen');
|
||
},
|
||
},
|
||
);
|
||
|
||
const { execute: executeReject, isPending: isRejecting } = useAction(
|
||
rejectApplication,
|
||
{
|
||
onSuccess: ({ data }) => {
|
||
if (data?.success) {
|
||
toast.success('Antrag wurde abgelehnt');
|
||
router.refresh();
|
||
}
|
||
},
|
||
onError: ({ error }) => {
|
||
toast.error(error.serverError ?? 'Fehler beim Ablehnen');
|
||
},
|
||
},
|
||
);
|
||
|
||
const handleApprove = useCallback(
|
||
(applicationId: string) => {
|
||
if (!window.confirm('Mitglied wird automatisch erstellt. Fortfahren?')) {
|
||
return;
|
||
}
|
||
executeApprove({ applicationId, accountId });
|
||
},
|
||
[executeApprove, accountId],
|
||
);
|
||
|
||
const handleReject = useCallback(
|
||
(applicationId: string) => {
|
||
const reason = window.prompt(
|
||
'Bitte geben Sie einen Ablehnungsgrund ein:',
|
||
);
|
||
if (reason === null) return; // cancelled
|
||
executeReject({
|
||
applicationId,
|
||
accountId,
|
||
reviewNotes: reason,
|
||
});
|
||
},
|
||
[executeReject, accountId],
|
||
);
|
||
|
||
const isPending = isApproving || isRejecting;
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-lg font-semibold">Aufnahmeanträge</h2>
|
||
<p className="text-muted-foreground text-sm">
|
||
{applications.length} Antrag{applications.length !== 1 ? 'e' : ''}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="rounded-md border">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="bg-muted/50 border-b">
|
||
<th scope="col" className="px-4 py-3 text-left font-medium">
|
||
Name
|
||
</th>
|
||
<th scope="col" className="px-4 py-3 text-left font-medium">
|
||
E-Mail
|
||
</th>
|
||
<th scope="col" className="px-4 py-3 text-left font-medium">
|
||
Datum
|
||
</th>
|
||
<th scope="col" className="px-4 py-3 text-left font-medium">
|
||
Status
|
||
</th>
|
||
<th scope="col" className="px-4 py-3 text-right font-medium">
|
||
Aktionen
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{applications.length === 0 ? (
|
||
<tr>
|
||
<td
|
||
colSpan={5}
|
||
className="text-muted-foreground px-4 py-8 text-center"
|
||
>
|
||
Keine Aufnahmeanträge vorhanden.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
applications.map((app) => {
|
||
const appId = String(app.id ?? '');
|
||
const appStatus = String(app.status ?? 'submitted');
|
||
const isActionable =
|
||
appStatus === 'submitted' || appStatus === 'review';
|
||
|
||
return (
|
||
<tr key={appId} className="border-b">
|
||
<td className="px-4 py-3">
|
||
{String(app.last_name ?? '')},{' '}
|
||
{String(app.first_name ?? '')}
|
||
</td>
|
||
<td className="text-muted-foreground px-4 py-3">
|
||
{String(app.email ?? '—')}
|
||
</td>
|
||
<td className="text-muted-foreground px-4 py-3">
|
||
{formatDate(app.created_at as string)}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<Badge
|
||
variant={
|
||
APPLICATION_STATUS_VARIANT[appStatus] ?? 'secondary'
|
||
}
|
||
>
|
||
{APPLICATION_STATUS_LABEL[appStatus] ?? appStatus}
|
||
</Badge>
|
||
</td>
|
||
<td className="px-4 py-3 text-right">
|
||
{isActionable && (
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
size="sm"
|
||
variant="default"
|
||
disabled={isPending}
|
||
data-test="application-approve-btn"
|
||
onClick={() => handleApprove(appId)}
|
||
>
|
||
Genehmigen
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="destructive"
|
||
disabled={isPending}
|
||
data-test="application-reject-btn"
|
||
onClick={() => handleReject(appId)}
|
||
>
|
||
Ablehnen
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|