247 lines
8.4 KiB
TypeScript
247 lines
8.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import {
|
|
Building2, Upload, X, Loader2, Shield, Trash2, AlertTriangle,
|
|
} from 'lucide-react'
|
|
|
|
interface OrgTabProps {
|
|
tenantId?: string | null
|
|
}
|
|
|
|
export function OrgTab({ tenantId }: OrgTabProps) {
|
|
const { toast } = useToast()
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [uploadingLogo, setUploadingLogo] = useState(false)
|
|
const [tenant, setTenant] = useState<any>(null)
|
|
|
|
// Editable fields
|
|
const [name, setName] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [contactEmail, setContactEmail] = useState('')
|
|
const [contactPhone, setContactPhone] = useState('')
|
|
const [address, setAddress] = useState('')
|
|
|
|
const fetchTenant = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch('/api/tenant/info')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const t = data.tenant
|
|
if (t) {
|
|
setTenant(t)
|
|
setName(t.name || '')
|
|
setDescription(t.description || '')
|
|
setContactEmail(t.contactEmail || '')
|
|
setContactPhone(t.contactPhone || '')
|
|
setAddress(t.address || '')
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load tenant info:', e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchTenant()
|
|
}, [tenantId])
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true)
|
|
try {
|
|
const res = await fetch('/api/tenant/info', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.trim(),
|
|
description: description.trim() || null,
|
|
contactEmail: contactEmail.trim() || null,
|
|
contactPhone: contactPhone.trim() || null,
|
|
address: address.trim() || null,
|
|
}),
|
|
})
|
|
if (res.ok) {
|
|
toast({ title: 'Organisation aktualisiert' })
|
|
fetchTenant()
|
|
} else {
|
|
const err = await res.json()
|
|
toast({ title: 'Fehler', description: err.error || 'Speichern fehlgeschlagen', variant: 'destructive' })
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', description: 'Verbindungsfehler', variant: 'destructive' })
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (!e.target.files?.[0]) return
|
|
setUploadingLogo(true)
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('logo', e.target.files[0])
|
|
const res = await fetch('/api/tenant/logo', { method: 'POST', body: formData })
|
|
if (res.ok) {
|
|
toast({ title: 'Logo hochgeladen' })
|
|
fetchTenant()
|
|
} else {
|
|
const data = await res.json()
|
|
toast({ title: 'Fehler', description: data.error || 'Upload fehlgeschlagen', variant: 'destructive' })
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', description: 'Upload fehlgeschlagen', variant: 'destructive' })
|
|
} finally {
|
|
setUploadingLogo(false)
|
|
e.target.value = ''
|
|
}
|
|
}
|
|
|
|
const handleLogoDelete = async () => {
|
|
try {
|
|
const res = await fetch('/api/tenant/logo', { method: 'DELETE' })
|
|
if (res.ok) {
|
|
toast({ title: 'Logo entfernt' })
|
|
fetchTenant()
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!tenant) {
|
|
return (
|
|
<div className="text-center text-muted-foreground py-12">
|
|
Keine Organisation zugeordnet.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-2xl">
|
|
{/* Logo */}
|
|
<div className="border rounded-lg p-5">
|
|
<h3 className="font-semibold text-base mb-3 flex items-center gap-2">
|
|
<Building2 className="w-4 h-4" />
|
|
Logo
|
|
</h3>
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-20 h-20 rounded-lg border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
|
{tenant.logoUrl ? (
|
|
<img
|
|
src={tenant.logoUrl.startsWith('/') ? tenant.logoUrl : `/api/tenant/logo/serve`}
|
|
alt="Logo"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
) : (
|
|
<Building2 className="w-10 h-10 text-muted-foreground/40" />
|
|
)}
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" className="relative" disabled={uploadingLogo}>
|
|
{uploadingLogo ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <Upload className="w-3.5 h-3.5 mr-1" />}
|
|
{tenant.logoUrl ? 'Ändern' : 'Hochladen'}
|
|
<input
|
|
type="file"
|
|
accept="image/png,image/jpeg,image/svg+xml,image/webp"
|
|
onChange={handleLogoUpload}
|
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
/>
|
|
</Button>
|
|
{tenant.logoUrl && (
|
|
<Button variant="ghost" size="sm" className="text-destructive" onClick={handleLogoDelete}>
|
|
<X className="w-3.5 h-3.5 mr-1" /> Entfernen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">PNG, JPEG, SVG oder WebP, max. 2 MB. Wird auch im Rapport angezeigt.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Organisation Details */}
|
|
<div className="border rounded-lg p-5 space-y-4">
|
|
<h3 className="font-semibold text-base mb-1 flex items-center gap-2">
|
|
<Shield className="w-4 h-4" />
|
|
Stammdaten
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">Name</Label>
|
|
<Input value={name} onChange={e => setName(e.target.value)} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Slug</Label>
|
|
<Input value={tenant.slug} disabled className="font-mono bg-muted" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Beschreibung</Label>
|
|
<Input value={description} onChange={e => setDescription(e.target.value)} placeholder="z.B. Feuerwehr Musterstadt" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">Kontakt E-Mail</Label>
|
|
<Input type="email" value={contactEmail} onChange={e => setContactEmail(e.target.value)} placeholder="kontakt@feuerwehr.ch" />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Kontakt Telefon</Label>
|
|
<Input value={contactPhone} onChange={e => setContactPhone(e.target.value)} placeholder="+41 ..." />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Adresse</Label>
|
|
<Input value={address} onChange={e => setAddress(e.target.value)} placeholder="Strasse, PLZ Ort" />
|
|
</div>
|
|
<Button onClick={handleSave} disabled={saving || !name.trim()}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : null}
|
|
Speichern
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Info (read-only) */}
|
|
<div className="border rounded-lg p-5">
|
|
<h3 className="font-semibold text-base mb-3">Übersicht</h3>
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Plan:</span>
|
|
<span className="ml-2">{tenant.plan}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Status:</span>
|
|
<span className="ml-2">{tenant.subscriptionStatus}</span>
|
|
</div>
|
|
{tenant._count && (
|
|
<>
|
|
<div>
|
|
<span className="text-muted-foreground">Benutzer:</span>
|
|
<span className="ml-2">{tenant._count.memberships}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Einsätze:</span>
|
|
<span className="ml-2">{tenant._count.projects}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|