Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -1,12 +1,14 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useAction } from 'next-safe-action/hooks';
import { useForm } from 'react-hook-form';
import { todayISO } from '@kit/shared/dates';
import { Button } from '@kit/ui/button';
import { Input } from '@kit/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import {
Form,
FormControl,
@@ -16,7 +18,7 @@ import {
FormMessage,
FormDescription,
} from '@kit/ui/form';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
@@ -39,7 +41,7 @@ export function CreateProtocolForm({
defaultValues: {
accountId,
title: '',
meetingDate: new Date().toISOString().split('T')[0]!,
meetingDate: todayISO(),
meetingType: 'vorstand' as const,
location: '',
attendees: '',
@@ -80,7 +82,10 @@ export function CreateProtocolForm({
<FormItem>
<FormLabel>Titel *</FormLabel>
<FormControl>
<Input placeholder="z.B. Vorstandssitzung März 2026" {...field} />
<Input
placeholder="z.B. Vorstandssitzung März 2026"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -111,7 +116,7 @@ export function CreateProtocolForm({
<FormControl>
<select
{...field}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
<option value="vorstand">Vorstandssitzung</option>
<option value="mitglieder">Mitgliederversammlung</option>
@@ -156,7 +161,7 @@ export function CreateProtocolForm({
<textarea
{...field}
placeholder="Namen der Teilnehmer (kommagetrennt oder zeilenweise)"
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
/>
</FormControl>
<FormMessage />
@@ -174,7 +179,7 @@ export function CreateProtocolForm({
<textarea
{...field}
placeholder="Optionale Anmerkungen zum Protokoll"
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
/>
</FormControl>
<FormMessage />
@@ -196,9 +201,12 @@ export function CreateProtocolForm({
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Protokoll veröffentlichen</FormLabel>
<FormLabel className="text-base">
Protokoll veröffentlichen
</FormLabel>
<FormDescription>
Veröffentlichte Protokolle sind für alle Mitglieder sichtbar.
Veröffentlichte Protokolle sind für alle Mitglieder
sichtbar.
</FormDescription>
</div>
<FormControl>

View File

@@ -2,13 +2,9 @@
import Link from 'next/link';
import {
FileText,
Calendar,
ListChecks,
AlertTriangle,
} from 'lucide-react';
import { FileText, Calendar, ListChecks, AlertTriangle } from 'lucide-react';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
@@ -71,10 +67,12 @@ export function MeetingsDashboard({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">Protokolle gesamt</p>
<p className="text-muted-foreground text-sm font-medium">
Protokolle gesamt
</p>
<p className="text-2xl font-bold">{stats.totalProtocols}</p>
</div>
<div className="rounded-full bg-primary/10 p-3 text-primary">
<div className="bg-primary/10 text-primary rounded-full p-3">
<FileText className="h-5 w-5" />
</div>
</div>
@@ -87,10 +85,14 @@ export function MeetingsDashboard({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">Protokolle dieses Jahr</p>
<p className="text-2xl font-bold">{stats.thisYearProtocols}</p>
<p className="text-muted-foreground text-sm font-medium">
Protokolle dieses Jahr
</p>
<p className="text-2xl font-bold">
{stats.thisYearProtocols}
</p>
</div>
<div className="rounded-full bg-primary/10 p-3 text-primary">
<div className="bg-primary/10 text-primary rounded-full p-3">
<Calendar className="h-5 w-5" />
</div>
</div>
@@ -103,10 +105,12 @@ export function MeetingsDashboard({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">Offene Aufgaben</p>
<p className="text-muted-foreground text-sm font-medium">
Offene Aufgaben
</p>
<p className="text-2xl font-bold">{stats.openTasks}</p>
</div>
<div className="rounded-full bg-primary/10 p-3 text-primary">
<div className="bg-primary/10 text-primary rounded-full p-3">
<ListChecks className="h-5 w-5" />
</div>
</div>
@@ -119,10 +123,12 @@ export function MeetingsDashboard({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">Überfällige Aufgaben</p>
<p className="text-muted-foreground text-sm font-medium">
Überfällige Aufgaben
</p>
<p className="text-2xl font-bold">{stats.overdueTasks}</p>
</div>
<div className="rounded-full bg-destructive/10 p-3 text-destructive">
<div className="bg-destructive/10 text-destructive rounded-full p-3">
<AlertTriangle className="h-5 w-5" />
</div>
</div>
@@ -139,7 +145,7 @@ export function MeetingsDashboard({
</CardHeader>
<CardContent>
{recentProtocols.length === 0 ? (
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
Noch keine Protokolle vorhanden.
</p>
) : (
@@ -148,18 +154,23 @@ export function MeetingsDashboard({
<Link
key={protocol.id}
href={`/home/${account}/meetings/protocols/${protocol.id}`}
className="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50"
className="hover:bg-muted/50 flex items-center justify-between rounded-lg border p-3 transition-colors"
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{protocol.title}</p>
<p className="text-xs text-muted-foreground">
{new Date(protocol.meeting_date).toLocaleDateString('de-DE')}
<p className="truncate text-sm font-medium">
{protocol.title}
</p>
<p className="text-muted-foreground text-xs">
{formatDate(protocol.meeting_date)}
{' · '}
{MEETING_TYPE_LABELS[protocol.meeting_type] ?? protocol.meeting_type}
{MEETING_TYPE_LABELS[protocol.meeting_type] ??
protocol.meeting_type}
</p>
</div>
{protocol.is_published && (
<Badge variant="default" className="ml-2 shrink-0">Veröffentlicht</Badge>
<Badge variant="default" className="ml-2 shrink-0">
Veröffentlicht
</Badge>
)}
</Link>
))}
@@ -174,7 +185,7 @@ export function MeetingsDashboard({
</CardHeader>
<CardContent>
{overdueTasks.length === 0 ? (
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
Keine überfälligen Aufgaben.
</p>
) : (
@@ -182,20 +193,18 @@ export function MeetingsDashboard({
{overdueTasks.map((task) => (
<div
key={task.id}
className="rounded-lg border border-destructive/20 bg-destructive/5 p-3"
className="border-destructive/20 bg-destructive/5 rounded-lg border p-3"
>
<p className="text-sm font-medium">{task.title}</p>
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
{task.responsible_person && (
<span>Zuständig: {task.responsible_person}</span>
)}
{task.due_date && (
<span>
Fällig: {new Date(task.due_date).toLocaleDateString('de-DE')}
</span>
<span>Fällig: {formatDate(task.due_date)}</span>
)}
</div>
<p className="mt-1 text-xs text-muted-foreground">
<p className="text-muted-foreground mt-1 text-xs">
Protokoll: {task.meeting_protocols.title}
</p>
</div>

View File

@@ -1,6 +1,7 @@
'use client';
import Link from 'next/link';
import { cn } from '@kit/ui/utils';
interface MeetingsTabNavigationProps {
@@ -22,7 +23,10 @@ export function MeetingsTabNavigation({
return (
<div className="mb-6 border-b">
<nav className="-mb-px flex space-x-1 overflow-x-auto" aria-label="Sitzungsprotokolle Navigation">
<nav
className="-mb-px flex space-x-1 overflow-x-auto"
aria-label="Sitzungsprotokolle Navigation"
>
{TABS.map((tab) => {
const isActive = tab.id === activeTab;
@@ -31,10 +35,10 @@ export function MeetingsTabNavigation({
key={tab.id}
href={`${basePath}${tab.path}`}
className={cn(
'whitespace-nowrap border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
isActive
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground',
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
)}
>
{tab.label}

View File

@@ -1,13 +1,18 @@
'use client';
import { useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { ITEM_STATUS_LABELS, ITEM_STATUS_COLORS } from '../lib/meetings-constants';
import {
ITEM_STATUS_LABELS,
ITEM_STATUS_COLORS,
} from '../lib/meetings-constants';
interface OpenTask {
id: string;
@@ -78,7 +83,7 @@ export function OpenTasksView({
{data.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
<h3 className="text-lg font-semibold">Keine offenen Aufgaben</h3>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
Alle Tagesordnungspunkte sind erledigt oder vertagt.
</p>
</div>
@@ -86,7 +91,7 @@ export function OpenTasksView({
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Aufgabe</th>
<th className="p-3 text-left font-medium">Protokoll</th>
<th className="p-3 text-left font-medium">Zuständig</th>
@@ -102,7 +107,7 @@ export function OpenTasksView({
return (
<tr
key={task.id}
className="cursor-pointer border-b hover:bg-muted/30"
className="hover:bg-muted/30 cursor-pointer border-b"
onClick={() =>
router.push(
`/home/${account}/meetings/protocols/${task.meeting_protocols.id}`,
@@ -113,27 +118,29 @@ export function OpenTasksView({
<div>
<p className="font-medium">{task.title}</p>
{task.description && (
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">
<p className="text-muted-foreground mt-0.5 line-clamp-1 text-xs">
{task.description}
</p>
)}
</div>
</td>
<td className="p-3 text-muted-foreground">
<td className="text-muted-foreground p-3">
<div>
<p className="text-sm">{task.meeting_protocols.title}</p>
<p className="text-sm">
{task.meeting_protocols.title}
</p>
<p className="text-xs">
{new Date(task.meeting_protocols.meeting_date).toLocaleDateString('de-DE')}
{formatDate(task.meeting_protocols.meeting_date)}
</p>
</div>
</td>
<td className="p-3 text-muted-foreground">
<td className="text-muted-foreground p-3">
{task.responsible_person ?? '—'}
</td>
<td className={`p-3 ${isOverdue ? 'font-medium text-destructive' : 'text-muted-foreground'}`}>
{task.due_date
? new Date(task.due_date).toLocaleDateString('de-DE')
: '—'}
<td
className={`p-3 ${isOverdue ? 'text-destructive font-medium' : 'text-muted-foreground'}`}
>
{task.due_date ? formatDate(task.due_date) : '—'}
{isOverdue && (
<span className="ml-1 text-xs">(überfällig)</span>
)}
@@ -141,8 +148,11 @@ export function OpenTasksView({
<td className="p-3 text-center">
<Badge
variant={
(ITEM_STATUS_COLORS[task.status] as 'default' | 'secondary' | 'destructive' | 'outline') ??
'outline'
(ITEM_STATUS_COLORS[task.status] as
| 'default'
| 'secondary'
| 'destructive'
| 'outline') ?? 'outline'
}
>
{ITEM_STATUS_LABELS[task.status] ?? task.status}
@@ -159,7 +169,7 @@ export function OpenTasksView({
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
Seite {page} von {totalPages} ({total} Einträge)
</p>
<div className="flex gap-2">

View File

@@ -2,15 +2,21 @@
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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { toast } from '@kit/ui/sonner';
import { ITEM_STATUS_LABELS, ITEM_STATUS_COLORS } from '../lib/meetings-constants';
import { updateItemStatus, deleteProtocolItem } from '../server/actions/meetings-actions';
import {
ITEM_STATUS_LABELS,
ITEM_STATUS_COLORS,
} from '../lib/meetings-constants';
import type { MeetingItemStatus } from '../schema/meetings.schema';
import {
updateItemStatus,
deleteProtocolItem,
} from '../server/actions/meetings-actions';
interface ProtocolItem {
id: string;
@@ -40,27 +46,33 @@ export function ProtocolItemsList({
protocolId,
account,
}: ProtocolItemsListProps) {
const { execute: executeStatusUpdate, isPending: isUpdating } = useAction(updateItemStatus, {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Status aktualisiert');
}
const { execute: executeStatusUpdate, isPending: isUpdating } = useAction(
updateItemStatus,
{
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Status aktualisiert');
}
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Aktualisieren');
},
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Aktualisieren');
},
});
);
const { execute: executeDelete, isPending: isDeleting } = useAction(deleteProtocolItem, {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Tagesordnungspunkt gelöscht');
}
const { execute: executeDelete, isPending: isDeleting } = useAction(
deleteProtocolItem,
{
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Tagesordnungspunkt gelöscht');
}
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Löschen');
},
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Löschen');
},
});
);
const handleToggleStatus = (item: ProtocolItem) => {
const nextStatus = STATUS_TRANSITIONS[item.status] ?? 'offen';
@@ -82,7 +94,7 @@ export function ProtocolItemsList({
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center">
<h3 className="text-lg font-semibold">Keine Tagesordnungspunkte</h3>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
Fügen Sie Tagesordnungspunkte zu diesem Protokoll hinzu.
</p>
</div>
@@ -90,7 +102,7 @@ export function ProtocolItemsList({
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">#</th>
<th className="p-3 text-left font-medium">Titel</th>
<th className="p-3 text-left font-medium">Zuständig</th>
@@ -107,31 +119,34 @@ export function ProtocolItemsList({
new Date(item.due_date) < new Date();
return (
<tr key={item.id} className="border-b hover:bg-muted/30">
<td className="p-3 text-muted-foreground">{index + 1}</td>
<tr key={item.id} className="hover:bg-muted/30 border-b">
<td className="text-muted-foreground p-3">{index + 1}</td>
<td className="p-3">
<div>
<p className="font-medium">{item.title}</p>
{item.description && (
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">
<p className="text-muted-foreground mt-0.5 line-clamp-1 text-xs">
{item.description}
</p>
)}
</div>
</td>
<td className="p-3 text-muted-foreground">
<td className="text-muted-foreground p-3">
{item.responsible_person ?? '—'}
</td>
<td className={`p-3 ${isOverdue ? 'font-medium text-destructive' : 'text-muted-foreground'}`}>
{item.due_date
? new Date(item.due_date).toLocaleDateString('de-DE')
: '—'}
<td
className={`p-3 ${isOverdue ? 'text-destructive font-medium' : 'text-muted-foreground'}`}
>
{formatDate(item.due_date)}
</td>
<td className="p-3 text-center">
<Badge
variant={
(ITEM_STATUS_COLORS[item.status] as 'default' | 'secondary' | 'destructive' | 'outline') ??
'outline'
(ITEM_STATUS_COLORS[item.status] as
| 'default'
| 'secondary'
| 'destructive'
| 'outline') ?? 'outline'
}
className="cursor-pointer"
onClick={() => handleToggleStatus(item)}

View File

@@ -1,12 +1,14 @@
'use client';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { Plus } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
@@ -107,7 +109,7 @@ export function ProtocolsDataTable({
<select
value={currentType}
onChange={handleTypeChange}
className="flex h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
>
{MEETING_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
@@ -133,11 +135,16 @@ export function ProtocolsDataTable({
<CardContent>
{data.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
<h3 className="text-lg font-semibold">Keine Protokolle vorhanden</h3>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
<h3 className="text-lg font-semibold">
Keine Protokolle vorhanden
</h3>
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
Erstellen Sie Ihr erstes Sitzungsprotokoll, um loszulegen.
</p>
<Link href={`/home/${account}/meetings/protocols/new`} className="mt-4">
<Link
href={`/home/${account}/meetings/protocols/new`}
className="mt-4"
>
<Button>Neues Protokoll</Button>
</Link>
</div>
@@ -145,7 +152,7 @@ export function ProtocolsDataTable({
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Datum</th>
<th className="p-3 text-left font-medium">Titel</th>
<th className="p-3 text-left font-medium">Sitzungsart</th>
@@ -157,17 +164,15 @@ export function ProtocolsDataTable({
{data.map((protocol) => (
<tr
key={String(protocol.id)}
className="cursor-pointer border-b hover:bg-muted/30"
className="hover:bg-muted/30 cursor-pointer border-b"
onClick={() =>
router.push(
`/home/${account}/meetings/protocols/${String(protocol.id)}`,
)
}
>
<td className="p-3 text-muted-foreground">
{protocol.meeting_date
? new Date(String(protocol.meeting_date)).toLocaleDateString('de-DE')
: '—'}
<td className="text-muted-foreground p-3">
{formatDate(protocol.meeting_date as string)}
</td>
<td className="p-3 font-medium">
<Link
@@ -183,7 +188,7 @@ export function ProtocolsDataTable({
String(protocol.meeting_type)}
</Badge>
</td>
<td className="p-3 text-muted-foreground">
<td className="text-muted-foreground p-3">
{String(protocol.location ?? '—')}
</td>
<td className="p-3 text-center">
@@ -203,7 +208,7 @@ export function ProtocolsDataTable({
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
Seite {page} von {totalPages} ({total} Einträge)
</p>
<div className="flex gap-2">

View File

@@ -37,7 +37,9 @@ export const CreateMeetingProtocolSchema = z.object({
isPublished: z.boolean().default(false),
});
export type CreateMeetingProtocolInput = z.infer<typeof CreateMeetingProtocolSchema>;
export type CreateMeetingProtocolInput = z.infer<
typeof CreateMeetingProtocolSchema
>;
export const UpdateMeetingProtocolSchema = z.object({
protocolId: z.string().uuid(),
@@ -50,7 +52,9 @@ export const UpdateMeetingProtocolSchema = z.object({
isPublished: z.boolean().optional(),
});
export type UpdateMeetingProtocolInput = z.infer<typeof UpdateMeetingProtocolSchema>;
export type UpdateMeetingProtocolInput = z.infer<
typeof UpdateMeetingProtocolSchema
>;
// =====================================================
// Protocol Item Schemas

View File

@@ -1,7 +1,9 @@
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { authActionClient } from '@kit/next/safe-action';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -14,7 +16,6 @@ import {
UpdateItemStatusSchema,
ReorderItemsSchema,
} from '../../schema/meetings.schema';
import { createMeetingsApi } from '../api';
const REVALIDATION_PATH = '/home/[account]/meetings';
@@ -31,7 +32,10 @@ export const createProtocol = authActionClient
const api = createMeetingsApi(client);
const userId = ctx.user.id;
logger.info({ name: 'meetings.protocol.create' }, 'Protokoll wird erstellt...');
logger.info(
{ name: 'meetings.protocol.create' },
'Protokoll wird erstellt...',
);
const result = await api.createProtocol(input, userId);
logger.info({ name: 'meetings.protocol.create' }, 'Protokoll erstellt');
revalidatePath(REVALIDATION_PATH, 'page');
@@ -46,7 +50,10 @@ export const updateProtocol = authActionClient
const api = createMeetingsApi(client);
const userId = ctx.user.id;
logger.info({ name: 'meetings.protocol.update' }, 'Protokoll wird aktualisiert...');
logger.info(
{ name: 'meetings.protocol.update' },
'Protokoll wird aktualisiert...',
);
const result = await api.updateProtocol(input, userId);
logger.info({ name: 'meetings.protocol.update' }, 'Protokoll aktualisiert');
revalidatePath(REVALIDATION_PATH, 'page');
@@ -65,7 +72,10 @@ export const deleteProtocol = authActionClient
const logger = await getLogger();
const api = createMeetingsApi(client);
logger.info({ name: 'meetings.protocol.delete' }, 'Protokoll wird gelöscht...');
logger.info(
{ name: 'meetings.protocol.delete' },
'Protokoll wird gelöscht...',
);
await api.deleteProtocol(input.protocolId);
logger.info({ name: 'meetings.protocol.delete' }, 'Protokoll gelöscht');
revalidatePath(REVALIDATION_PATH, 'page');
@@ -84,9 +94,15 @@ export const createProtocolItem = authActionClient
const api = createMeetingsApi(client);
const userId = ctx.user.id;
logger.info({ name: 'meetings.item.create' }, 'Tagesordnungspunkt wird erstellt...');
logger.info(
{ name: 'meetings.item.create' },
'Tagesordnungspunkt wird erstellt...',
);
const result = await api.createItem(input, userId);
logger.info({ name: 'meetings.item.create' }, 'Tagesordnungspunkt erstellt');
logger.info(
{ name: 'meetings.item.create' },
'Tagesordnungspunkt erstellt',
);
revalidatePath(REVALIDATION_PATH, 'page');
return { success: true, data: result };
});
@@ -99,9 +115,15 @@ export const updateProtocolItem = authActionClient
const api = createMeetingsApi(client);
const userId = ctx.user.id;
logger.info({ name: 'meetings.item.update' }, 'Tagesordnungspunkt wird aktualisiert...');
logger.info(
{ name: 'meetings.item.update' },
'Tagesordnungspunkt wird aktualisiert...',
);
const result = await api.updateItem(input, userId);
logger.info({ name: 'meetings.item.update' }, 'Tagesordnungspunkt aktualisiert');
logger.info(
{ name: 'meetings.item.update' },
'Tagesordnungspunkt aktualisiert',
);
revalidatePath(REVALIDATION_PATH, 'page');
return { success: true, data: result };
});
@@ -132,9 +154,15 @@ export const deleteProtocolItem = authActionClient
const logger = await getLogger();
const api = createMeetingsApi(client);
logger.info({ name: 'meetings.item.delete' }, 'Tagesordnungspunkt wird gelöscht...');
logger.info(
{ name: 'meetings.item.delete' },
'Tagesordnungspunkt wird gelöscht...',
);
await api.deleteItem(input.itemId);
logger.info({ name: 'meetings.item.delete' }, 'Tagesordnungspunkt gelöscht');
logger.info(
{ name: 'meetings.item.delete' },
'Tagesordnungspunkt gelöscht',
);
revalidatePath(REVALIDATION_PATH, 'page');
return { success: true };
});
@@ -146,7 +174,10 @@ export const reorderProtocolItems = authActionClient
const logger = await getLogger();
const api = createMeetingsApi(client);
logger.info({ name: 'meetings.items.reorder' }, 'Reihenfolge wird aktualisiert...');
logger.info(
{ name: 'meetings.items.reorder' },
'Reihenfolge wird aktualisiert...',
);
await api.reorderItems(input);
logger.info({ name: 'meetings.items.reorder' }, 'Reihenfolge aktualisiert');
revalidatePath(REVALIDATION_PATH, 'page');
@@ -173,7 +204,10 @@ export const addProtocolAttachment = authActionClient
const api = createMeetingsApi(client);
const userId = ctx.user.id;
logger.info({ name: 'meetings.attachment.add' }, 'Anhang wird hinzugefügt...');
logger.info(
{ name: 'meetings.attachment.add' },
'Anhang wird hinzugefügt...',
);
const result = await api.addAttachment(
input.protocolId,
input.fileName,
@@ -198,7 +232,10 @@ export const deleteProtocolAttachment = authActionClient
const logger = await getLogger();
const api = createMeetingsApi(client);
logger.info({ name: 'meetings.attachment.delete' }, 'Anhang wird gelöscht...');
logger.info(
{ name: 'meetings.attachment.delete' },
'Anhang wird gelöscht...',
);
await api.deleteAttachment(input.attachmentId);
logger.info({ name: 'meetings.attachment.delete' }, 'Anhang gelöscht');
revalidatePath(REVALIDATION_PATH, 'page');

View File

@@ -1,6 +1,7 @@
import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import type {
CreateMeetingProtocolInput,
UpdateMeetingProtocolInput,
@@ -98,12 +99,15 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
const updateData: Record<string, unknown> = { updated_by: userId };
if (input.title !== undefined) updateData.title = input.title;
if (input.meetingDate !== undefined) updateData.meeting_date = input.meetingDate;
if (input.meetingType !== undefined) updateData.meeting_type = input.meetingType;
if (input.meetingDate !== undefined)
updateData.meeting_date = input.meetingDate;
if (input.meetingType !== undefined)
updateData.meeting_type = input.meetingType;
if (input.location !== undefined) updateData.location = input.location;
if (input.attendees !== undefined) updateData.attendees = input.attendees;
if (input.remarks !== undefined) updateData.remarks = input.remarks;
if (input.isPublished !== undefined) updateData.is_published = input.isPublished;
if (input.isPublished !== undefined)
updateData.is_published = input.isPublished;
const { data, error } = await client
.from('meeting_protocols')
@@ -127,10 +131,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
// Protocol Items (Tagesordnungspunkte)
// =====================================================
async listItems(
protocolId: string,
opts?: { status?: string },
) {
async listItems(protocolId: string, opts?: { status?: string }) {
let query = client
.from('meeting_protocol_items')
.select(
@@ -173,11 +174,14 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
const updateData: Record<string, unknown> = { updated_by: userId };
if (input.title !== undefined) updateData.title = input.title;
if (input.description !== undefined) updateData.description = input.description;
if (input.responsiblePerson !== undefined) updateData.responsible_person = input.responsiblePerson;
if (input.description !== undefined)
updateData.description = input.description;
if (input.responsiblePerson !== undefined)
updateData.responsible_person = input.responsiblePerson;
if (input.dueDate !== undefined) updateData.due_date = input.dueDate;
if (input.status !== undefined) updateData.status = input.status;
if (input.sortOrder !== undefined) updateData.sort_order = input.sortOrder;
if (input.sortOrder !== undefined)
updateData.sort_order = input.sortOrder;
const { data, error } = await client
.from('meeting_protocol_items')
@@ -327,19 +331,19 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
// Open tasks (offen + in_bearbeitung)
client
.from('meeting_protocol_items')
.select(
'id, meeting_protocols!inner ( account_id )',
{ count: 'exact', head: true },
)
.select('id, meeting_protocols!inner ( account_id )', {
count: 'exact',
head: true,
})
.eq('meeting_protocols.account_id', accountId)
.in('status', ['offen', 'in_bearbeitung']),
// Overdue tasks
client
.from('meeting_protocol_items')
.select(
'id, meeting_protocols!inner ( account_id )',
{ count: 'exact', head: true },
)
.select('id, meeting_protocols!inner ( account_id )', {
count: 'exact',
head: true,
})
.eq('meeting_protocols.account_id', accountId)
.in('status', ['offen', 'in_bearbeitung'])
.lt('due_date', new Date().toISOString().split('T')[0]!),

View File

@@ -1,6 +1,8 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" },
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "*.tsx", "src"],
"exclude": ["node_modules"]
}