229 lines
9.7 KiB
TypeScript
229 lines
9.7 KiB
TypeScript
"use client"
|
|
|
|
import type { ReactNode } from "react"
|
|
import { useAuth } from "@/lib/auth-context"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuCheckboxItem,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { LogOut, User, Building2, Cpu, ShieldAlert, KeyRound } from "lucide-react"
|
|
import { ModeToggle } from "@/components/mode-toggle"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Input } from "@/components/ui/input"
|
|
import { useState } from "react"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
|
|
// Defines props for the AppShell component
|
|
interface AppShellProps {
|
|
children: ReactNode // React children to be rendered within the shell
|
|
}
|
|
|
|
// AppShell component provides the main layout structure for the application
|
|
export function AppShell({ children }: AppShellProps) {
|
|
// Access authentication context for user info, logout function, room data, and room update function
|
|
const { user, logout, rooms, updateRooms, changePassword } = useAuth()
|
|
const { toast } = useToast()
|
|
|
|
const [changePasswordOpen, setChangePasswordOpen] = useState(false)
|
|
const [currentPassword, setCurrentPassword] = useState("")
|
|
const [newPassword, setNewPassword] = useState("")
|
|
|
|
const handleChangePassword = async () => {
|
|
if (!currentPassword || !newPassword) return
|
|
const success = await changePassword(currentPassword, newPassword)
|
|
if (success) {
|
|
toast({ title: "Passwort geändert" })
|
|
setChangePasswordOpen(false)
|
|
setCurrentPassword("")
|
|
setNewPassword("")
|
|
} else {
|
|
toast({ title: "Fehler", description: "Passwort konnte nicht geändert werden", variant: "destructive" })
|
|
}
|
|
}
|
|
|
|
// Generates user initials for the avatar fallback
|
|
const initials = user?.name
|
|
?.split(" ")
|
|
.map((n) => n[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2) || "" // Fallback to empty string if user name is not available
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
{/* Header section with sticky positioning, blur effect, and responsiveness */}
|
|
<header className="sticky top-0 z-50 w-full border-b border-border/60 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="w-full max-w-[95%] mx-auto flex h-16 items-center justify-between px-4">
|
|
{/* Application logo and title */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary">
|
|
<Cpu className="h-5 w-5 text-primary-foreground" />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="font-semibold tracking-tight">Elektronikschule</span>
|
|
<span className="text-xs text-muted-foreground">IT-Support-Portal</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* User actions and theme toggle */}
|
|
<div className="flex items-center gap-4">
|
|
{/* Display user role for larger screens */}
|
|
<div className="hidden items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5 md:flex">
|
|
{user?.role === "ADMIN" ? (
|
|
<ShieldAlert className="h-4 w-4 text-destructive" />
|
|
) : user?.role === "LEHRKRAFT" ? (
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
<span className={`text-sm font-medium capitalize ${user?.role === "ADMIN" ? "text-destructive" : ""}`}>
|
|
{user?.role === "ADMIN"
|
|
? "Admin"
|
|
: user?.role === "LEHRKRAFT"
|
|
? "Lehrkraft"
|
|
: "Raumbetreuer"}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Dropdown menu for user profile and settings */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="relative h-9 w-9 rounded-full">
|
|
<Avatar className="h-9 w-9">
|
|
<AvatarFallback className="bg-primary text-primary-foreground text-sm font-medium">
|
|
{initials}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium leading-none">{user?.name}</p>
|
|
<p className="text-xs leading-none text-muted-foreground">{user?.email}</p>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{/* Display user role in the dropdown */}
|
|
<DropdownMenuItem className="text-muted-foreground">
|
|
{user?.role === "ADMIN" ? <ShieldAlert className="mr-2 h-4 w-4 text-destructive" /> : <User className="mr-2 h-4 w-4" />}
|
|
<span className={`capitalize ${user?.role === "ADMIN" ? "text-destructive font-bold" : ""}`}>
|
|
{user?.role === "ADMIN" ? "Admin" : user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onClick={() => setChangePasswordOpen(true)}>
|
|
<KeyRound className="mr-2 h-4 w-4" />
|
|
<span>Passwort ändern</span>
|
|
</DropdownMenuItem>
|
|
|
|
{/* Room supervision management for 'RAUMBETREUER' role */}
|
|
{user?.role === "RAUMBETREUER" && user.supervisedRooms && (
|
|
<DropdownMenuSub>
|
|
<DropdownMenuSubTrigger>
|
|
<Building2 className="mr-2 h-4 w-4" />
|
|
<span>{user.supervisedRooms.length} Betreute Räume</span>
|
|
</DropdownMenuSubTrigger>
|
|
<DropdownMenuSubContent className="max-h-60 overflow-y-auto">
|
|
<DropdownMenuLabel>Räume verwalten</DropdownMenuLabel>
|
|
{rooms.map((room) => {
|
|
// Determine if the current room is supervised by the user
|
|
const isChecked = user.supervisedRooms?.some((r) => r.id === room.id) ?? false
|
|
return (
|
|
<DropdownMenuCheckboxItem
|
|
key={room.id}
|
|
checked={isChecked}
|
|
onCheckedChange={(checked) => {
|
|
const currentIds = user.supervisedRooms?.map((r) => r.id) || []
|
|
let newIds
|
|
if (checked) {
|
|
// Add room to supervised list
|
|
newIds = [...currentIds, room.id]
|
|
} else {
|
|
// Remove room from supervised list
|
|
newIds = currentIds.filter((id) => id !== room.id)
|
|
}
|
|
updateRooms(newIds) // Update the list of supervised rooms
|
|
}}
|
|
onSelect={(e) => e.preventDefault()} // Prevent closing dropdown on selection
|
|
>
|
|
{room.name}
|
|
</DropdownMenuCheckboxItem>
|
|
)
|
|
})}
|
|
</DropdownMenuSubContent>
|
|
</DropdownMenuSub>
|
|
)}
|
|
<DropdownMenuSeparator />
|
|
{/* Logout button */}
|
|
<DropdownMenuItem onClick={logout} className="text-destructive focus:text-destructive">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>Abmelden</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Theme toggle component */}
|
|
<ModeToggle />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<Dialog open={changePasswordOpen} onOpenChange={setChangePasswordOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Passwort ändern</DialogTitle>
|
|
<DialogDescription>
|
|
Geben Sie Ihr aktuelles Passwort und das neue Passwort ein.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="current">Aktuelles Passwort</Label>
|
|
<Input
|
|
id="current"
|
|
type="password"
|
|
value={currentPassword}
|
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="new">Neues Passwort</Label>
|
|
<Input
|
|
id="new"
|
|
type="password"
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setChangePasswordOpen(false)}>Abbrechen</Button>
|
|
<Button onClick={handleChangePassword}>Speichern</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Main content area */}
|
|
<main className="w-full max-w-[95%] mx-auto px-4 py-8">{children}</main>
|
|
</div>
|
|
)
|
|
}
|