Files
schulos/src/app/admin/signage/page.tsx
T
Z User 91d6980c21 i18n: Fix 19 more hardcoded English strings in admin pages and components
Fixed:
- compliance-overview: Compliance Monitoring Dashboard, Onboarding Status, Data Retention Policies, Active Policies, Consultation Log
- exam-proctoring: Exam Proctoring, Active Sessions, Total Sessions
- signage: Emergency Signage, Normal option
- compliance-wizard: Data Retention Periods, Consultation Checklist, Compliance Setup Complete
- dashboard-customizer: Add Widget
- data-import-export: Data Management Best Practices, Export data regularly
- district-management: Access Denied
- exam-session-monitor: Exam Mode Active
- professional-calendar: Fixed 'Create Neues Ereignis' typo
- sidebar: Toggle Sidebar

All admin pages and shared components now fully German.
2026-08-11 09:46:05 +00:00

353 lines
13 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { DataTable } from '@/components/data-table';
import { ConfirmDialog } from '@/components/confirm-dialog';
import { useApiGet, useApiMutation } from '@/lib/hooks/useApi';
import { getPriorityColor, getMessageTypeIcon, formatDisplayTime } from '@/lib/signage-utils';
import { AlertCircle, Tv, Plus } from 'lucide-react';
export default function SignageAdminPage() {
// Get schoolId from URL params or context
const [schoolId, setSchoolId] = React.useState<string | null>(null);
React.useEffect(() => {
// Get schoolId from session or localStorage
const storedSchoolId = typeof window !== 'undefined' ? localStorage.getItem('schoolId') : null;
setSchoolId(storedSchoolId);
}, []);
const [showNewMessage, setShowNewMessage] = useState(false);
const [selectedSignage, setSelectedSignage] = useState<string | null>(null);
const [showConfirm, setShowConfirm] = useState(false);
// Fetch all signages
const { data: signages, mutate: refetchSignages } = useApiGet(
schoolId ? `/api/v1/signage?schoolId=${schoolId}` : null
);
// Fetch messages for selected signage
const { data: messages, mutate: refetchMessages } = useApiGet(
selectedSignage && schoolId
? `/api/v1/signage/${selectedSignage}/messages`
: null
);
const { mutate: createMessage, isLoading: isCreating } = useApiMutation(
`/api/v1/signage/${selectedSignage}/messages`,
{
method: 'POST',
onSuccess: () => {
setShowNewMessage(false);
refetchMessages();
},
}
);
const { mutate: deleteMessage, isLoading: isDeleting } = useApiMutation(
`/api/v1/signage/messages/delete`,
{
method: 'DELETE',
onSuccess: () => {
setShowConfirm(false);
refetchMessages();
},
}
);
const handleCreateMessage = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const message = {
title: formData.get('title'),
content: formData.get('content'),
messageType: formData.get('messageType'),
priority: formData.get('priority'),
displayDuration: parseInt(formData.get('displayDuration') as string) || 10,
soundAlert: formData.get('soundAlert') === 'on',
};
await createMessage(message);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Notfall-Digital-Schild</h1>
<p className="text-gray-600 mt-1">Manage displays, messages, and emergency alerts</p>
</div>
<Button onClick={() => setShowNewMessage(true)} size="lg" className="gap-2">
<Plus className="h-5 w-5" />
New Message
</Button>
</div>
<Tabs defaultValue="displays" className="w-full">
<TabsList>
<TabsTrigger value="displays">Bildschirme</TabsTrigger>
<TabsTrigger value="messages">Nachrichten</TabsTrigger>
<TabsTrigger value="activity">Aktivitätsprotokoll</TabsTrigger>
</TabsList>
{/* Displays Tab */}
<TabsContent value="displays" className="space-y-4">
{signages && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{signages.map((signage: any) => (
<Card
key={signage.id}
className="cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => setSelectedSignage(signage.id)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg">{signage.displayName}</CardTitle>
<CardDescription>{signage.location}</CardDescription>
</div>
<Tv className="h-5 w-5 text-gray-400" />
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<p className="text-gray-600">Status</p>
<p className={signage.isActive ? 'text-green-600 font-medium' : 'text-red-600 font-medium'}>
{signage.isActive ? 'Online' : 'Offline'}
</p>
</div>
<div>
<p className="text-gray-600">Type</p>
<p className="text-gray-900 font-medium">{signage.signageType}</p>
</div>
</div>
<div className="pt-2 border-t">
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => setSelectedSignage(signage.id)}
>
Manage
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
{/* Messages Tab */}
<TabsContent value="messages" className="space-y-4">
{selectedSignage ? (
<>
{messages && (
<DataTable
columns={[
{
key: 'title' as any,
label: 'Title',
render: (_, row) => (
<div className="flex items-center gap-2">
<span>{getMessageTypeIcon(row.messageType)}</span>
<span>{row.title}</span>
</div>
),
},
{
key: 'priority' as any,
label: 'Priority',
render: (_, row) => (
<span
className="px-3 py-1 rounded-full text-sm font-medium text-white"
style={{ backgroundColor: getPriorityColor(row.priority) }}
>
{row.priority.toUpperCase()}
</span>
),
},
{
key: 'displayDuration' as any,
label: 'Duration',
render: (_, row) => formatDisplayTime(row.displayDuration || 10),
},
{
key: 'soundAlert' as any,
label: 'Sound',
render: (_, row) => (
<span className={row.soundAlert ? 'text-amber-600' : 'text-gray-400'}>
{row.soundAlert ? 'volume-up' : 'volume-off'}
</span>
),
},
]}
data={messages}
actions={[
{
label: 'Send Now',
onClick: (row) => console.log('Send', row.id),
},
{
label: 'Edit',
onClick: (row) => console.log('Edit', row.id),
},
{
label: 'Delete',
onClick: (row) => {
setShowConfirm(true);
},
variant: 'destructive',
},
]}
/>
)}
</>
) : (
<Card>
<CardContent className="pt-6 text-center text-gray-500">
<AlertCircle className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Wählen Sie einen Bildschirm zur Verwaltung von Nachrichten</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* Activity Log Tab */}
<TabsContent value="activity">
<Card>
<CardHeader>
<CardTitle>Aktivitätsprotokoll</CardTitle>
<CardDescription>Aktuelle Digital-Schild-Ereignisse und Warnungen</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center text-gray-500 py-8">
Activity log feature coming soon
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* New Message Dialog */}
{showNewMessage && selectedSignage && (
<Card className="fixed inset-4 z-50 md:inset-auto md:top-1/2 md:left-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:max-w-md max-h-[90vh] overflow-y-auto">
<CardHeader>
<CardTitle>Neue Nachricht</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateMessage} className="space-y-4">
<div>
<Label htmlFor="title">Titel</Label>
<Input
id="title"
name="title"
placeholder="Nachrichtentitel"
required
/>
</div>
<div>
<Label htmlFor="content">Inhalt</Label>
<textarea
id="content"
name="content"
placeholder="Nachrichteninhalt"
className="w-full h-24 px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<Label htmlFor="messageType">Typ</Label>
<select
id="messageType"
name="messageType"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
required
>
<option value="announcement">Ankündigung</option>
<option value="alert">Warnung</option>
<option value="emergency">Notfall</option>
<option value="notification">Benachrichtigung</option>
</select>
</div>
<div>
<Label htmlFor="priority">Priorität</Label>
<select
id="priority"
name="priority"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
required
>
<option value="low">Niedrig</option>
<option value="normal">Normal</option>
<option value="high">Hoch</option>
<option value="critical">Kritisch</option>
</select>
</div>
<div>
<Label htmlFor="displayDuration">Display Duration (seconds)</Label>
<Input
id="displayDuration"
name="displayDuration"
type="number"
defaultValue="10"
min="1"
max="300"
/>
</div>
<div className="flex items-center gap-2">
<input
id="soundAlert"
name="soundAlert"
type="checkbox"
className="rounded"
/>
<Label htmlFor="soundAlert" className="mb-0">Play sound alert</Label>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setShowNewMessage(false)}
className="flex-1"
>
Cancel
</Button>
<Button type="submit" disabled={isCreating} className="flex-1">
{isCreating ? 'Creating...' : 'Create Message'}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* Confirm Dialog */}
<ConfirmDialog
open={showConfirm}
title="Delete Message"
description="This action cannot be undone."
onConfirm={async () => {
// Delete logic
setShowConfirm(false);
}}
onCancel={() => setShowConfirm(false)}
variant="destructive"
/>
</div>
);
}