Files
tickte-system/src/pages/DashboardPage.jsx
Webklar Deploy d20a4de117 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>
2026-06-17 09:41:33 +00:00

106 lines
5.1 KiB
JavaScript

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, 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 { 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(() => {
let active = true
;(async () => {
try {
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.orderDesc('$createdAt'), Query.limit(6)]),
])
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 {
if (active) setLoading(false)
}
try {
const d = await integrationsApi.listDocuments({ page_size: 1 })
if (active) setDocCount(d.count)
} catch { /* ignore */ }
})()
return () => { active = false }
}, [])
return (
<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="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="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>
)
}