Initial commit - ERP System

This commit is contained in:
root
2026-05-20 18:58:23 +00:00
commit e174936997
2697 changed files with 1628427 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
// /opt/erp-system/app/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { useSession } from "next-auth/react";
import { Ticket as TicketIcon, Clock, Activity, ArrowRight, Plus, X, CheckCircle2 } from 'lucide-react';
import Link from 'next/link';
import { getStatusBadge } from './components/AppShell';
export default function DashboardPage() {
const { data: session } = useSession();
const [data, setData] = useState<any>(null);
const [error, setError] = useState(false);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({ title: '', description: '' });
useEffect(() => {
fetchData();
}, []);
const fetchData = () => {
fetch('/api/dashboard')
.then(res => {
if (!res.ok) throw new Error('Netzwerkfehler');
return res.json();
})
.then(setData)
.catch(() => setError(true));
};
const handleCustomerTicketSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: formData.title,
description: formData.description,
customerId: (session?.user as any)?.id
})
});
if (res.ok) {
setShowForm(false);
setFormData({ title: '', description: '' });
fetchData();
} else {
alert('Fehler beim Erstellen des Tickets');
}
};
if (error) return <div className="p-8 text-red-600 font-medium">Fehler beim Laden des Dashboards.</div>;
if (!data) return <div className="p-8 text-slate-500 font-medium animate-pulse">Lade Metriken...</div>;
const userType = (session?.user as any)?.userType;
// ----------------------------------------------------
// INTERFACE FÜR KUNDEN
// ----------------------------------------------------
if (userType === 'CUSTOMER') {
return (
<div className="space-y-8 animate-fade-in-up stagger-1">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900">
Hallo, {(session?.user as any)?.firstName}!
</h1>
<p className="text-slate-500 mt-1">Willkommen in deinem Service-Portal. Hier kannst du deine Support-Anfragen verwalten.</p>
</div>
<button
onClick={() => setShowForm(!showForm)}
className="bg-indigo-600 text-white px-5 py-2.5 rounded-lg font-medium shadow-sm flex items-center gap-2 hover:bg-indigo-700 transition self-start md:self-auto"
>
{showForm ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
{showForm ? 'Abbrechen' : 'Neues Ticket eröffnen'}
</button>
</div>
{showForm && (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200 animate-in fade-in slide-in-from-top-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Unterstützung anfordern</h2>
<form onSubmit={handleCustomerTicketSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff / Thema *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={formData.title} onChange={e => setFormData({...formData, title: e.target.value})} placeholder="Wobei benötigst du Hilfe?" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung des Problems *</label>
<textarea required className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 resize-none" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} placeholder="Bitte beschreibe das Problem so genau wie möglich..." />
</div>
<div className="flex justify-end">
<button type="submit" className="bg-slate-900 text-white px-6 py-2.5 rounded-lg hover:bg-slate-800 transition font-medium">Ticket absenden</button>
</div>
</form>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-start gap-4 hover-lift">
<div className="p-3 bg-amber-50 text-amber-600 rounded-lg">
<TicketIcon className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">In Bearbeitung / Offen</p>
<p className="text-2xl font-bold text-slate-900">{data.openTickets}</p>
</div>
</div>
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-start gap-4 hover-lift">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-lg">
<CheckCircle2 className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Gelöste Tickets</p>
<p className="text-2xl font-bold text-slate-900">{data.closedTickets}</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up stagger-2">
<div className="p-6 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-800">Deine Tickets</h2>
</div>
<div className="divide-y divide-slate-100">
{data.recentTickets.length > 0 ? data.recentTickets.map((ticket: any) => (
<div key={ticket.id} className="p-4 px-6 flex items-center justify-between hover:bg-slate-50 transition group">
<div>
<Link href={`/tickets/${ticket.id}`} className="font-semibold text-slate-900 group-hover:text-indigo-600 transition">
#{ticket.id} {ticket.title}
</Link>
<p className="text-sm text-slate-500 mt-0.5">
Erstellt am: {new Date(ticket.createdAt).toLocaleDateString('de-DE')} Letztes Update: {new Date(ticket.updatedAt).toLocaleString('de-DE')}
</p>
</div>
<div>
{getStatusBadge(ticket.status)}
</div>
</div>
)) : (
<div className="p-8 text-center text-slate-500">Du hast bisher noch keine Tickets erstellt.</div>
)}
</div>
</div>
</div>
);
}
// ----------------------------------------------------
// INTERFACE FÜR TEAM
// ----------------------------------------------------
return (
<div className="max-w-7xl mx-auto space-y-8 animate-fade-in-up stagger-1">
<div>
<h1 className="text-2xl font-bold text-slate-900">
Willkommen zurück, {(session?.user as any)?.firstName}!
</h1>
<p className="text-slate-500 mt-1">Hier ist der aktuelle Status deines ERP-Systems.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-start gap-4 hover-lift">
<div className="p-3 bg-red-50 text-red-600 rounded-lg">
<TicketIcon className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Offene Tickets (Gesamt)</p>
<p className="text-3xl font-extrabold text-slate-900 mt-1">{data.openTickets}</p>
</div>
<div className="ml-auto flex items-end gap-1 h-12">
{[40, 70, 45, 90, 65, 80].map((h, i) => (
<div key={i} className="w-1.5 bg-red-200 rounded-t-sm animate-grow-bar" style={{ height: `${h}%`, animationDelay: `${i * 100}ms` }}></div>
))}
</div>
</div>
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-start gap-4 hover-lift">
<div className="p-3 bg-indigo-50 text-indigo-600 rounded-lg">
<Activity className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Dir zugewiesen (Offen)</p>
<p className="text-3xl font-extrabold text-slate-900 mt-1">{data.myTickets}</p>
</div>
<div className="ml-auto flex items-end gap-1 h-12">
{[20, 40, 30, 60, 50, 70].map((h, i) => (
<div key={i} className="w-1.5 bg-indigo-200 rounded-t-sm animate-grow-bar" style={{ height: `${h}%`, animationDelay: `${i * 100}ms` }}></div>
))}
</div>
</div>
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-start gap-4 hover-lift">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-lg">
<Clock className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Erfasste Stunden (Gesamt)</p>
<p className="text-3xl font-extrabold text-slate-900 mt-1">{data.totalHours.toFixed(1)} <span className="text-lg text-slate-400 font-medium">h</span></p>
</div>
<div className="ml-auto flex items-end gap-1 h-12">
{[30, 50, 40, 80, 60, 90].map((h, i) => (
<div key={i} className="w-1.5 bg-emerald-200 rounded-t-sm animate-grow-bar" style={{ height: `${h}%`, animationDelay: `${i * 100}ms` }}></div>
))}
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up stagger-2">
<div className="p-6 border-b border-slate-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-slate-800">Zuletzt aktualisierte Tickets</h2>
<Link href="/tickets" className="text-sm font-medium text-indigo-600 hover:text-indigo-800 flex items-center gap-1">
Alle ansehen <ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="divide-y divide-slate-100">
{data.recentTickets.length > 0 ? data.recentTickets.map((ticket: any) => (
<div key={ticket.id} className="p-4 px-6 flex items-center justify-between hover:bg-slate-50 transition group">
<div>
<Link href={`/tickets/${ticket.id}`} className="font-semibold text-slate-900 group-hover:text-indigo-600 transition">
#{ticket.id} {ticket.title}
</Link>
<p className="text-sm text-slate-500 mt-0.5">
{ticket.customer.companyName || `${ticket.customer.firstName} ${ticket.customer.lastName}`} Update: {new Date(ticket.updatedAt).toLocaleString('de-DE')}
</p>
</div>
<div>
{getStatusBadge(ticket.status)}
</div>
</div>
)) : (
<div className="p-8 text-center text-slate-500">Keine Tickets vorhanden.</div>
)}
</div>
</div>
</div>
);
}