v1.1.0: keyboard shortcuts (CH), onboarding tour, admin projects tab, remember-me login, Luftbild CH removed, hose settings in admin, credit link, font Barlow, map auto-save viewport, rate-limit 10/5min
This commit is contained in:
@@ -58,6 +58,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { TenantDetailDialog } from '@/components/admin/tenant-detail-dialog'
|
||||
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
|
||||
|
||||
// --- Types ---
|
||||
interface IconCategory {
|
||||
@@ -214,6 +215,14 @@ export default function AdminPage() {
|
||||
const [symbolScaleLoading, setSymbolScaleLoading] = useState(false)
|
||||
const [symbolScaleStatus, setSymbolScaleStatus] = useState<string | null>(null)
|
||||
|
||||
// Admin Projects (SERVER_ADMIN)
|
||||
const [adminProjects, setAdminProjects] = useState<any[]>([])
|
||||
const [adminProjectsLoading, setAdminProjectsLoading] = useState(false)
|
||||
const [adminProjectTenantFilter, setAdminProjectTenantFilter] = useState<string>('all')
|
||||
|
||||
// Hose Settings (Tenant Admin)
|
||||
const [isHoseSettingsOpen, setIsHoseSettingsOpen] = useState(false)
|
||||
|
||||
// Redirect to login if not authenticated, or to app if not admin
|
||||
useEffect(() => {
|
||||
if (authLoading) return
|
||||
@@ -251,6 +260,25 @@ export default function AdminPage() {
|
||||
if (user?.role === 'SERVER_ADMIN') fetchGlobalDict()
|
||||
}, [user?.role])
|
||||
|
||||
// Fetch admin projects (SERVER_ADMIN)
|
||||
const fetchAdminProjects = async (tenantFilter?: string) => {
|
||||
setAdminProjectsLoading(true)
|
||||
try {
|
||||
const url = tenantFilter && tenantFilter !== 'all'
|
||||
? `/api/admin/projects?tenantId=${tenantFilter}`
|
||||
: '/api/admin/projects'
|
||||
const res = await fetch(url)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setAdminProjects(data.projects || [])
|
||||
}
|
||||
} catch {}
|
||||
setAdminProjectsLoading(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (user?.role === 'SERVER_ADMIN') fetchAdminProjects()
|
||||
}, [user?.role])
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
@@ -683,11 +711,15 @@ export default function AdminPage() {
|
||||
<div className="container mx-auto py-6 px-4 max-w-7xl">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
{user?.role === 'SERVER_ADMIN' ? (
|
||||
<TabsList className="grid w-full grid-cols-6 max-w-3xl">
|
||||
<TabsList className="grid w-full grid-cols-7 max-w-4xl">
|
||||
<TabsTrigger value="tenants" className="gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
Mandanten
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="projects" className="gap-2">
|
||||
<Map className="w-4 h-4" />
|
||||
Einsätze
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="icons" className="gap-2">
|
||||
<Image className="w-4 h-4" />
|
||||
Symbole
|
||||
@@ -710,7 +742,7 @@ export default function AdminPage() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
) : user?.role === 'TENANT_ADMIN' ? (
|
||||
<TabsList className="grid w-full grid-cols-5 max-w-2xl">
|
||||
<TabsList className="grid w-full grid-cols-6 max-w-3xl">
|
||||
<TabsTrigger value="users" className="gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
Benutzer
|
||||
@@ -719,6 +751,10 @@ export default function AdminPage() {
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
Wörterliste
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hose-types" className="gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
Schläuche
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="donate" className="gap-2">
|
||||
<Heart className="w-4 h-4" />
|
||||
Spenden
|
||||
@@ -960,6 +996,97 @@ export default function AdminPage() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* ===== PROJECTS TAB (SERVER_ADMIN — Einsätze verwalten) ===== */}
|
||||
{user?.role === 'SERVER_ADMIN' && (
|
||||
<TabsContent value="projects" className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{adminProjects.length} Einsatz/Einsätze
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Feuerwehr:</span>
|
||||
<Select value={adminProjectTenantFilter} onValueChange={(val) => { setAdminProjectTenantFilter(val); fetchAdminProjects(val) }}>
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue placeholder="Alle Mandanten" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Alle Mandanten</SelectItem>
|
||||
{tenants.map(t => (
|
||||
<SelectItem key={t.id} value={t.id}>{t.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{adminProjectsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : adminProjects.length === 0 ? (
|
||||
<p className="text-center text-muted-foreground py-8">Keine Einsätze gefunden.</p>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Einsatz-Nr</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Titel</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Ort</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Erstellt von</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Feuerwehr</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Elemente</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Geändert</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{adminProjects.map((p: any) => (
|
||||
<tr key={p.id} className="hover:bg-muted/30">
|
||||
<td className="px-4 py-2.5 font-mono text-xs">{p.einsatzNr || '—'}</td>
|
||||
<td className="px-4 py-2.5 font-semibold">{p.title}</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground truncate max-w-[200px]">{p.location || '—'}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className="text-xs">{p.owner?.name || p.owner?.email || '—'}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">{p.tenant?.name || '—'}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-center">{p._count?.features || 0}</td>
|
||||
<td className="px-4 py-2.5 text-xs text-muted-foreground">{new Date(p.updatedAt).toLocaleString('de-CH')}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={() => window.open(`/app?project=${p.id}`, '_blank')}>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
Öffnen
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* ===== HOSE TYPES TAB (Schlauchtypen) ===== */}
|
||||
<TabsContent value="hose-types" className="space-y-4">
|
||||
<div className="border rounded-lg p-6">
|
||||
<h3 className="font-semibold text-lg mb-2 flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Schlauchtypen verwalten
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Konfiguriere die Schlauchtypen für die Druckberechnung im Messwerkzeug. Der Standard-Schlauch wird automatisch für neue Berechnungen verwendet.
|
||||
</p>
|
||||
<Button variant="outline" onClick={() => setIsHoseSettingsOpen(true)}>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Schlauchtypen bearbeiten
|
||||
</Button>
|
||||
</div>
|
||||
<HoseSettingsDialog open={isHoseSettingsOpen} onOpenChange={setIsHoseSettingsOpen} />
|
||||
</TabsContent>
|
||||
|
||||
{/* ===== SUGGESTIONS TAB (Word Library) ===== */}
|
||||
<TabsContent value="suggestions" className="space-y-4">
|
||||
<div className="border rounded-lg p-6">
|
||||
|
||||
Reference in New Issue
Block a user