feat: Komplett-Redesign + Ausbau zur Plattform
- Ruhiges Dark-Designsystem (Tokens, Karten, Badges/Pills, KPIs) in global.css, animierter PixelBlast-Hintergrund entfernt (kleineres Bundle) - Aufgeraeumter App-Rahmen: feste Sidebar mit sichtbaren Labels, gruppierter Navigation, aktivem Zustand und Collapse - Wiederverwendbare UI-Bausteine unter src/components/ui/ - Ticketliste als scanbare Karten (TicketCard) statt rowSpan-Tabelle - Neue Bereiche: Kunden (CRM-360), Finanzen-Cockpit, Dokumente-Browser - Dashboard als echtes Cockpit (Ticket-/Finanz-/Dokument-Kennzahlen) - Alle Seiten auf einheitlichen PageHeader/Container/Tokens umgestellt Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,72 +1,104 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import { FaTicket, FaClipboardList, FaClock, FaCircleCheck } from 'react-icons/fa6'
|
||||
import { FaTicket, FaClipboardList, FaClock, FaCircleCheck, FaPlus, FaFileInvoiceDollar, FaUpload } from 'react-icons/fa6'
|
||||
import { integrationsApi, formatMoney } from '../lib/integrationsApi'
|
||||
import { useFinanceSummary } from '../hooks/useFinanceSummary'
|
||||
import { PageHeader, Card, Button, Kpi, EmptyState } from '../components/ui'
|
||||
import { StatusPill, PriorityPill } from '../components/ui/StatusPill'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState({
|
||||
totalTickets: 0,
|
||||
openTickets: 0,
|
||||
closedTickets: 0,
|
||||
pendingTickets: 0
|
||||
})
|
||||
const { summary, loading: finLoading } = useFinanceSummary()
|
||||
const [stats, setStats] = useState({ total: 0, open: 0, awaiting: 0, closed: 0 })
|
||||
const [recent, setRecent] = useState([])
|
||||
const [docCount, setDocCount] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
let active = true
|
||||
;(async () => {
|
||||
try {
|
||||
const [total, open, closed, pending] = await Promise.all([
|
||||
const [total, open, closed, awaiting, recentRes] = await Promise.all([
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.limit(1)]),
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Open'), Query.limit(1)]),
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Closed'), Query.limit(1)]),
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Awaiting'), Query.limit(1)])
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Awaiting'), Query.limit(1)]),
|
||||
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.orderDesc('$createdAt'), Query.limit(6)]),
|
||||
])
|
||||
|
||||
setStats({
|
||||
totalTickets: total.total,
|
||||
openTickets: open.total,
|
||||
closedTickets: closed.total,
|
||||
pendingTickets: pending.total
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err)
|
||||
if (!active) return
|
||||
setStats({ total: total.total, open: open.total, awaiting: awaiting.total, closed: closed.total })
|
||||
setRecent(recentRes.documents)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (active) setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchStats()
|
||||
try {
|
||||
const d = await integrationsApi.listDocuments({ page_size: 1 })
|
||||
if (active) setDocCount(d.count)
|
||||
} catch { /* ignore */ }
|
||||
})()
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
|
||||
const StatCard = ({ icon: Icon, label, value, color }) => (
|
||||
<div className="stat-card" style={{ borderLeft: `4px solid ${color}` }}>
|
||||
<div className="stat-icon" style={{ color }}>
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{loading ? '...' : value}</span>
|
||||
<span className="stat-label">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<header className="text-center mb-2">
|
||||
<h2>Dashboard</h2>
|
||||
</header>
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
subtitle="Ueberblick ueber Tickets, Finanzen und Dokumente"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="ghost" as={Link} to="/finance"><FaFileInvoiceDollar /> Finanzen</Button>
|
||||
<Button variant="primary" as={Link} to="/tickets"><FaPlus /> Neues Ticket</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="stats-grid">
|
||||
<StatCard icon={FaTicket} label="Total Tickets" value={stats.totalTickets} color="#2196F3" />
|
||||
<StatCard icon={FaClipboardList} label="Open Tickets" value={stats.openTickets} color="#4CAF50" />
|
||||
<StatCard icon={FaClock} label="Pending" value={stats.pendingTickets} color="#FF9800" />
|
||||
<StatCard icon={FaCircleCheck} label="Closed" value={stats.closedTickets} color="#9E9E9E" />
|
||||
<div className="kpi-grid">
|
||||
<Kpi label={<><FaTicket /> Tickets gesamt</>} value={loading ? '...' : stats.total} accent="var(--info)" />
|
||||
<Kpi label={<><FaClipboardList /> Offen</>} value={loading ? '...' : stats.open} accent="var(--ok)" />
|
||||
<Kpi label={<><FaClock /> Wartend</>} value={loading ? '...' : stats.awaiting} accent="var(--warn)" />
|
||||
<Kpi label={<><FaCircleCheck /> Geschlossen</>} value={loading ? '...' : stats.closed} accent="var(--text-faint)" />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-section mt-4">
|
||||
<h3>Quick Actions</h3>
|
||||
<div className="quick-actions">
|
||||
<a href="/tickets" className="btn btn-teal">View All Tickets</a>
|
||||
<a href="/reports" className="btn btn-dark">Generate Report</a>
|
||||
</div>
|
||||
<div className="kpi-grid">
|
||||
<Kpi label="Offener Betrag" value={finLoading ? '...' : formatMoney(summary?.outstanding)} sub={`${summary?.openCount ?? 0} offene Rechnungen`} accent="var(--warn)" />
|
||||
<Kpi label="Ueberfaellig" value={finLoading ? '...' : formatMoney(summary?.overdue)} sub={`${summary?.overdueCount ?? 0} ueberfaellig`} accent="var(--danger)" />
|
||||
<Kpi label="Bezahlt (Monat)" value={finLoading ? '...' : formatMoney(summary?.paidThisMonth)} accent="var(--ok)" />
|
||||
<Kpi label="Dokumente" value={docCount ?? '...'} accent="var(--info)" />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 16 }}>
|
||||
<Card title="Letzte Tickets" pad={false}>
|
||||
{loading ? (
|
||||
<div className="empty">...</div>
|
||||
) : recent.length === 0 ? (
|
||||
<EmptyState title="Keine Tickets" />
|
||||
) : (
|
||||
<div className="list">
|
||||
{recent.map((t) => (
|
||||
<Link key={t.$id} to="/tickets" className="list-row">
|
||||
<span className="mono" style={{ color: 'var(--accent)', minWidth: 56 }}>{t.woid || t.$id.slice(-5)}</span>
|
||||
<div className="grow">
|
||||
<div className="text-ellipsis" style={{ fontWeight: 600 }}>{t.topic || t.title || '-'}</div>
|
||||
<div className="muted">{t.customerName || '-'}</div>
|
||||
</div>
|
||||
<PriorityPill priority={t.priority} />
|
||||
<StatusPill status={t.status} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Schnellaktionen">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button as={Link} to="/tickets"><FaPlus /> Neues Ticket erstellen</Button>
|
||||
<Button as={Link} to="/finance"><FaFileInvoiceDollar /> Rechnung erstellen</Button>
|
||||
<Button as={Link} to="/documents"><FaUpload /> Dokument hochladen</Button>
|
||||
<Button as={Link} to="/customers" variant="ghost">Kunden verwalten</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user