192 lines
9.1 KiB
TypeScript
192 lines
9.1 KiB
TypeScript
// /opt/erp-system/app/tickets/page.tsx
|
|
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useSession } from "next-auth/react";
|
|
import { Ticket as TicketIcon, Plus, X } from 'lucide-react';
|
|
import { getStatusBadge, getPriorityBadge } from '../components/AppShell';
|
|
import { useToast } from '../components/ToastProvider';
|
|
|
|
export default function TicketsPage() {
|
|
const { data: session } = useSession();
|
|
const [tickets, setTickets] = useState<any[]>([]);
|
|
const [customers, setCustomers] = useState<any[]>([]);
|
|
const [filter, setFilter] = useState<'ALL' | 'MINE' | 'OPEN'>('ALL');
|
|
|
|
// States für das Formular
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [formData, setFormData] = useState({ title: '', description: '', customerId: '', priority: 'MEDIUM' });
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
fetchTickets();
|
|
fetchCustomers();
|
|
}, []);
|
|
|
|
const fetchTickets = async () => {
|
|
const res = await fetch('/api/tickets');
|
|
if (res.ok) setTickets(await res.json());
|
|
};
|
|
|
|
const fetchCustomers = async () => {
|
|
const res = await fetch('/api/customers');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setCustomers(data);
|
|
if (data.length > 0) {
|
|
setFormData(prev => ({ ...prev, customerId: data[0].id.toString() }));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const res = await fetch('/api/tickets', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
if (res.ok) {
|
|
setShowForm(false);
|
|
setFormData({ title: '', description: '', customerId: customers[0]?.id.toString() || '', priority: 'MEDIUM' });
|
|
fetchTickets();
|
|
toast('Ticket erfolgreich erstellt', 'success');
|
|
} else {
|
|
toast('Fehler beim Erstellen des Tickets', 'error');
|
|
}
|
|
};
|
|
|
|
// getStatusBadge is imported from AppShell
|
|
|
|
// Filter-Logik
|
|
const filteredTickets = tickets.filter(t => {
|
|
if (filter === 'OPEN') return t.status === 'OPEN' || t.status === 'IN_PROGRESS';
|
|
if (filter === 'MINE') return t.assignedToId === parseInt((session?.user as any)?.id);
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto space-y-6 animate-fade-in-up">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
|
<TicketIcon className="w-6 h-6 text-indigo-600" /> Ticketsystem
|
|
</h1>
|
|
<button onClick={() => setShowForm(!showForm)} className="bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium shadow-sm flex items-center gap-2 hover:bg-indigo-700 transition">
|
|
{showForm ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />} {showForm ? 'Abbrechen' : 'Neues Ticket'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Neues Ticket Formular */}
|
|
{showForm && (
|
|
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 animate-in fade-in slide-in-from-top-4">
|
|
<h2 className="text-lg font-semibold text-slate-800 mb-4">Neues Ticket erfassen</h2>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff *</label>
|
|
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={formData.title} onChange={e => setFormData({...formData, title: e.target.value})} placeholder="Kurze Beschreibung des Problems..." />
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Kunde *</label>
|
|
<select required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none bg-white transition-all" value={formData.customerId} onChange={e => setFormData({...formData, customerId: e.target.value})}>
|
|
{customers.map(c => (
|
|
<option key={c.id} value={c.id}>{c.companyName || `${c.firstName} ${c.lastName}`}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Priorität *</label>
|
|
<select required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none bg-white transition-all" value={formData.priority} onChange={e => setFormData({...formData, priority: e.target.value})}>
|
|
<option value="LOW">Niedrig</option>
|
|
<option value="MEDIUM">Mittel</option>
|
|
<option value="HIGH">Hoch</option>
|
|
<option value="CRITICAL">Kritisch</option>
|
|
</select>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Details *</label>
|
|
<textarea required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none h-32 resize-none transition-all" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} placeholder="Ausführliche Problembeschreibung..." />
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end pt-2">
|
|
<button type="submit" className="bg-slate-900 text-white px-6 py-2.5 rounded-lg hover:bg-slate-800 transition font-medium">Ticket erstellen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Filter Tabs */}
|
|
<div className="flex border-b border-slate-200 gap-6">
|
|
{[
|
|
{ id: 'ALL', label: 'Alle Tickets' },
|
|
{ id: 'MINE', label: 'Meine Tickets' },
|
|
{ id: 'OPEN', label: 'Nur Offene' }
|
|
].map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setFilter(tab.id as any)}
|
|
className={`pb-3 text-sm font-medium transition-colors relative ${filter === tab.id ? 'text-indigo-600' : 'text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
{tab.label}
|
|
{filter === tab.id && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600 animate-in fade-in duration-300"></div>}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Datentabelle */}
|
|
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden">
|
|
<table className="w-full text-left text-sm">
|
|
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
|
|
<tr>
|
|
<th className="py-4 px-6">ID</th>
|
|
<th className="py-4 px-6">Titel & Kunde</th>
|
|
<th className="py-4 px-6">Bearbeiter</th>
|
|
<th className="py-4 px-6">Status & Prio</th>
|
|
<th className="py-4 px-6 text-right">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{filteredTickets.map(t => (
|
|
<tr key={t.id} className="hover:bg-slate-50/80 transition-colors border-b border-slate-50 last:border-0 group">
|
|
<td className="py-4 px-6 font-mono text-slate-400">#{t.id.toString().padStart(5, '0')}</td>
|
|
<td className="py-4 px-6">
|
|
<div className="flex flex-col">
|
|
<span className="font-semibold text-slate-900">{t.title}</span>
|
|
<span className="text-slate-500 text-xs">{t.customer.companyName || `${t.customer.firstName} ${t.customer.lastName}`}</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-6">
|
|
{t.assignedTo ? (
|
|
<span className="flex items-center gap-1.5 text-slate-700">
|
|
<div className="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center text-[10px] font-bold text-indigo-700 border border-indigo-200">
|
|
{t.assignedTo.firstName[0]}
|
|
</div>
|
|
{t.assignedTo.firstName} {t.assignedTo.lastName}
|
|
</span>
|
|
) : <span className="text-slate-400 italic text-xs">Unzugewiesen</span>}
|
|
</td>
|
|
<td className="py-4 px-6">
|
|
<div className="flex flex-col gap-2 items-start">
|
|
{getStatusBadge(t.status)}
|
|
{getPriorityBadge(t.priority)}
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-6 text-right">
|
|
<a href={`/tickets/${t.id}`} className="text-indigo-600 font-medium hover:text-indigo-800">Akte öffnen</a>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{filteredTickets.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="py-8 text-center text-slate-500">Keine Tickets in dieser Ansicht gefunden.</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|