ni
This commit is contained in:
parent
7baf11e115
commit
3e95e2d4f2
9 changed files with 399 additions and 12 deletions
|
|
@ -2,24 +2,106 @@ package de.itsolutions.ticketsystem.controller;
|
||||||
|
|
||||||
import de.itsolutions.ticketsystem.entity.Room;
|
import de.itsolutions.ticketsystem.entity.Room;
|
||||||
import de.itsolutions.ticketsystem.repository.RoomRepository;
|
import de.itsolutions.ticketsystem.repository.RoomRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/rooms")
|
@RequestMapping("/api/rooms")
|
||||||
public class RoomController {
|
public class RoomController {
|
||||||
|
|
||||||
private final RoomRepository roomRepository;
|
@Autowired
|
||||||
|
private RoomRepository roomRepository;
|
||||||
public RoomController(RoomRepository roomRepository) {
|
|
||||||
this.roomRepository = roomRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<Room>> getAllRooms() {
|
public List<Room> getAllRooms() {
|
||||||
return ResponseEntity.ok(roomRepository.findAll());
|
return roomRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Room createRoom(@RequestBody Map<String, String> payload) {
|
||||||
|
String name = payload.get("name");
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Name is required");
|
||||||
|
}
|
||||||
|
if (roomRepository.findByName(name).isPresent()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Room already exists");
|
||||||
|
}
|
||||||
|
Room room = new Room();
|
||||||
|
room.setName(name);
|
||||||
|
return roomRepository.save(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Room updateRoom(@PathVariable Long id, @RequestBody Map<String, String> payload) {
|
||||||
|
String name = payload.get("name");
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Name is required");
|
||||||
|
}
|
||||||
|
Room room = roomRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found"));
|
||||||
|
|
||||||
|
// Check uniqueness if name changed
|
||||||
|
if (!room.getName().equals(name) && roomRepository.findByName(name).isPresent()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Room name already taken");
|
||||||
|
}
|
||||||
|
|
||||||
|
room.setName(name);
|
||||||
|
return roomRepository.save(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteRoom(@PathVariable Long id) {
|
||||||
|
if (!roomRepository.existsById(id)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found");
|
||||||
|
}
|
||||||
|
// Note: This might fail if constraints exist (tickets linked to room).
|
||||||
|
// Real-world app needs constraint handling (delete tickets or block).
|
||||||
|
// For simplicity here, we assume cascade or block.
|
||||||
|
// Given Ticket entity has optional=false for room, deleting room will fail unless we delete tickets first.
|
||||||
|
// Let's rely on DB error for now or add cascade logic later if requested.
|
||||||
|
try {
|
||||||
|
roomRepository.deleteById(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Cannot delete room with existing tickets");
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
public ResponseEntity<?> importRooms(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("File is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
int count = 0;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
String name = line.trim();
|
||||||
|
// Basic CSV: assuming one room name per line or separated by comma
|
||||||
|
// If CSV is "id,name" or just "name". Let's assume just "name" per line or typical CSV format handling.
|
||||||
|
// Simple implementation: One room name per line.
|
||||||
|
if (!name.isEmpty() && roomRepository.findByName(name).isEmpty()) {
|
||||||
|
Room room = new Room();
|
||||||
|
room.setName(name);
|
||||||
|
roomRepository.save(room);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("message", count + " rooms imported"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to process file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,10 @@ public class TicketController {
|
||||||
public ResponseEntity<Ticket> updateStatus(@PathVariable Long id, @RequestBody Dtos.TicketStatusRequest request) {
|
public ResponseEntity<Ticket> updateStatus(@PathVariable Long id, @RequestBody Dtos.TicketStatusRequest request) {
|
||||||
return ResponseEntity.ok(ticketService.updateTicketStatus(id, request.getStatus()));
|
return ResponseEntity.ok(ticketService.updateTicketStatus(id, request.getStatus()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteTicket(@PathVariable Long id) {
|
||||||
|
ticketService.deleteTicket(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,8 @@ package de.itsolutions.ticketsystem.repository;
|
||||||
import de.itsolutions.ticketsystem.entity.Room;
|
import de.itsolutions.ticketsystem.entity.Room;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface RoomRepository extends JpaRepository<Room, Long> {
|
public interface RoomRepository extends JpaRepository<Room, Long> {
|
||||||
|
Optional<Room> findByName(String name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,20 @@ public class TicketService {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return ticketRepository.findByRoomIdInOrderByCreatedAtDesc(roomIds);
|
return ticketRepository.findByRoomIdInOrderByCreatedAtDesc(roomIds);
|
||||||
|
} else if ("ADMIN".equals(user.getRole())) {
|
||||||
|
return ticketRepository.findAll();
|
||||||
}
|
}
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteTicket(Long id) {
|
||||||
|
if (ticketRepository.existsById(id)) {
|
||||||
|
ticketRepository.deleteById(id);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Ticket not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Ticket updateTicketStatus(Long ticketId, String status) {
|
public Ticket updateTicketStatus(Long ticketId, String status) {
|
||||||
Ticket ticket = ticketRepository.findById(ticketId)
|
Ticket ticket = ticketRepository.findById(ticketId)
|
||||||
.orElseThrow(() -> new RuntimeException("Ticket not found"));
|
.orElseThrow(() -> new RuntimeException("Ticket not found"));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { AuthProvider, useAuth } from "@/lib/auth-context"
|
import { AuthProvider, useAuth } from "@/lib/auth-context"
|
||||||
import { AuthScreen } from "@/components/auth/auth-screen"
|
import { AuthScreen } from "@/components/auth/auth-screen"
|
||||||
import { AppShell } from "@/components/layout/app-shell"
|
import { AppShell } from "@/components/layout/app-shell"
|
||||||
|
import { AdminDashboard } from "@/components/dashboard/admin-dashboard"
|
||||||
import { TeacherDashboard } from "@/components/dashboard/teacher-dashboard"
|
import { TeacherDashboard } from "@/components/dashboard/teacher-dashboard"
|
||||||
import { SupervisorDashboard } from "@/components/dashboard/supervisor-dashboard"
|
import { SupervisorDashboard } from "@/components/dashboard/supervisor-dashboard"
|
||||||
|
|
||||||
|
|
@ -13,6 +14,14 @@ function AppContent() {
|
||||||
return <AuthScreen />
|
return <AuthScreen />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.role === "ADMIN") {
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<AdminDashboard />
|
||||||
|
</AppShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
{user.role === "LEHRKRAFT" ? <TeacherDashboard /> : <SupervisorDashboard />}
|
{user.role === "LEHRKRAFT" ? <TeacherDashboard /> : <SupervisorDashboard />}
|
||||||
|
|
|
||||||
70
Frontend/components/dashboard/admin-dashboard.tsx
Normal file
70
Frontend/components/dashboard/admin-dashboard.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useAuth } from "@/lib/auth-context"
|
||||||
|
import { TicketTable } from "@/components/tickets/ticket-table"
|
||||||
|
import { RoomManagement } from "@/components/dashboard/room-management"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { ShieldAlert, Users } from "lucide-react"
|
||||||
|
|
||||||
|
export function AdminDashboard() {
|
||||||
|
const { tickets, updateTicketStatus, authHeader } = useAuth()
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL + "/api"
|
||||||
|
|
||||||
|
// Admin sees all tickets (logic handled in Backend/TicketService)
|
||||||
|
// We need to implement Ticket Delete logic passed to TicketTable or handle it inside TicketTable?
|
||||||
|
// TicketTable doesn't have Delete button yet.
|
||||||
|
// Actually, requirement says "Kann Tickets löschen". TicketTable needs a delete option or we add it.
|
||||||
|
|
||||||
|
// Let's add delete to TicketTable? OR just add it to the Detail Dialog?
|
||||||
|
// User asked "user with ID 1 = administrator... screen for creating/deleting/renaming rooms".
|
||||||
|
// For tickets, "Can delete tickets".
|
||||||
|
// I should probably add a Delete button in TicketTable (maybe only visible if onDelete is provided).
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Admin Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">Systemverwaltung und Ticket-Übersicht</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Alle Tickets</CardTitle>
|
||||||
|
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{tickets.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Gesamt im System</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">System Status</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">Aktiv</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Admin Mode</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RoomManagement />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ticket Übersicht</CardTitle>
|
||||||
|
<CardDescription>Alle Tickets aller Nutzer</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TicketTable
|
||||||
|
tickets={tickets}
|
||||||
|
showStatusUpdate
|
||||||
|
onStatusUpdate={updateTicketStatus}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
207
Frontend/components/dashboard/room-management.tsx
Normal file
207
Frontend/components/dashboard/room-management.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useAuth } from "@/lib/auth-context"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Trash2, Plus, Upload } from "lucide-react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import type { Room } from "@/lib/types"
|
||||||
|
|
||||||
|
export function RoomManagement() {
|
||||||
|
const { rooms, authHeader } = useAuth()
|
||||||
|
const [localRooms, setLocalRooms] = useState<Room[]>([])
|
||||||
|
const [newRoomName, setNewRoomName] = useState("")
|
||||||
|
const [isCreateOpen, setIsCreateOpen] = useState(false)
|
||||||
|
const [csvFile, setCsvFile] = useState<File | null>(null)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL + "/api"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalRooms(rooms)
|
||||||
|
}, [rooms])
|
||||||
|
|
||||||
|
const refreshRooms = async () => {
|
||||||
|
// Force reload (window reload is simplest for now to update Context, or expose fetchRooms in Context)
|
||||||
|
// Ideally Context should expose a 'refreshRooms' method.
|
||||||
|
// For now, let's update local list manually after actions or rely on hard refresh if needed.
|
||||||
|
// Actually, fetching from API here is better.
|
||||||
|
if (!authHeader) return
|
||||||
|
const res = await fetch(`${API_URL}/rooms`, { headers: { Authorization: authHeader } })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setLocalRooms(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!newRoomName.trim() || !authHeader) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/rooms`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": authHeader,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newRoomName })
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setNewRoomName("")
|
||||||
|
setIsCreateOpen(false)
|
||||||
|
refreshRooms()
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
if (!confirm("Raum wirklich löschen? Dies könnte fehlschlagen, wenn Tickets existieren.") || !authHeader) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/rooms/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Authorization": authHeader }
|
||||||
|
})
|
||||||
|
if (res.ok) refreshRooms()
|
||||||
|
else alert("Löschen fehlgeschlagen (evtl. existieren Tickets für diesen Raum)")
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = async (id: number, newName: string) => {
|
||||||
|
if (!authHeader) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/rooms/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Authorization": authHeader,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName })
|
||||||
|
})
|
||||||
|
if (res.ok) refreshRooms()
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = async () => {
|
||||||
|
if (!csvFile || !authHeader) return
|
||||||
|
setUploading(true)
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", csvFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/rooms/import`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Authorization": authHeader },
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
alert(data.message)
|
||||||
|
setCsvFile(null)
|
||||||
|
refreshRooms()
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
finally { setUploading(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Räume verwalten</CardTitle>
|
||||||
|
<CardDescription>Erstellen, Bearbeiten, Löschen und CSV-Import</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button size="sm"><Plus className="mr-2 h-4 w-4" /> Neuer Raum</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Neuen Raum erstellen</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Raum Name (z.B. A-101)"
|
||||||
|
value={newRoomName}
|
||||||
|
onChange={(e) => setNewRoomName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleCreate}>Erstellen</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
|
||||||
|
{/* CSV Import Section */}
|
||||||
|
<div className="flex items-center gap-4 p-4 border border-dashed rounded-lg bg-muted/30">
|
||||||
|
<div className="grid gap-1.5 flex-1">
|
||||||
|
<span className="font-semibold text-sm">Raum-Import (CSV)</span>
|
||||||
|
<span className="text-xs text-muted-foreground">Datei mit Raumnamen pro Zeile</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".csv,.txt"
|
||||||
|
onChange={(e) => setCsvFile(e.target.files?.[0] || null)}
|
||||||
|
className="w-full max-w-xs"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!csvFile || uploading}
|
||||||
|
onClick={handleFileUpload}
|
||||||
|
>
|
||||||
|
<Upload className="mr-2 h-4 w-4" /> Importieren
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead className="w-full">Name</TableHead>
|
||||||
|
<TableHead className="text-right">Aktionen</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{localRooms.map((room) => (
|
||||||
|
<TableRow key={room.id}>
|
||||||
|
<TableCell>{room.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
defaultValue={room.name}
|
||||||
|
className="h-8 w-40"
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (e.target.value !== room.name) {
|
||||||
|
handleUpdate(room.id, e.target.value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
onClick={() => handleDelete(room.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -161,7 +161,7 @@ export function TicketTable({ tickets, showStatusUpdate = false, onStatusUpdate
|
||||||
value={ticket.status}
|
value={ticket.status}
|
||||||
onValueChange={(value: TicketStatus) => onStatusUpdate(ticket.id, value)}
|
onValueChange={(value: TicketStatus) => onStatusUpdate(ticket.id, value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-32 h-8">
|
<SelectTrigger className="w-[180px] h-8">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export type UserRole = "LEHRKRAFT" | "RAUMBETREUER"
|
export type UserRole = "LEHRKRAFT" | "RAUMBETREUER" | "ADMIN"
|
||||||
|
|
||||||
export type TicketStatus = "OPEN" | "IN_PROGRESS" | "CLOSED"
|
export type TicketStatus = "OPEN" | "IN_PROGRESS" | "CLOSED"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue