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:
Webklar DeployandCursor committed 2026-06-17 09:41:33 +00:00
1 parent 8f6927fbb0
commit d20a4de117
28 files changed
+1639 -840

No files matched your search

+84 -380
View File
@@ -1,42 +1,30 @@
import { useState } from 'react'
import { format } from 'date-fns'
import { FaAngleDown, FaSpinner } from 'react-icons/fa6'
import { FaAngleDown, FaSpinner, FaPlus, FaList, FaSliders } from 'react-icons/fa6'
import { useWorkorders } from '../hooks/useWorkorders'
import { useCustomers } from '../hooks/useCustomers'
import TicketRow from '../components/TicketRow'
import TicketCard from '../components/TicketCard'
import CreateTicketModal from '../components/CreateTicketModal'
import QuickOverviewModal from '../components/QuickOverviewModal'
import { PageHeader, Card, Button, EmptyState } from '../components/ui'
const STATUS_PRESETS = ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info']
export default function TicketsPage() {
const [limit, setLimit] = useState(10)
// Aktive Filter (werden für API-Calls verwendet)
const [filters, setFilters] = useState({
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
type: [],
priority: [],
limit: 10
})
// Lokale Filter-Eingaben (werden nur beim Apply angewendet)
const [filters, setFilters] = useState({ status: STATUS_PRESETS, type: [], priority: [], limit: 10 })
const [localFilters, setLocalFilters] = useState({
woid: '',
customer: '',
userTopic: '',
createdDate: '',
type: '',
system: '',
priority: ''
woid: '', customer: '', userTopic: '', createdDate: '', type: '', system: '', priority: '',
})
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
const { customers } = useCustomers()
const [showCreateModal, setShowCreateModal] = useState(false)
const [showOverviewModal, setShowOverviewModal] = useState(false)
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
const [showAdvanced, setShowAdvanced] = useState(false)
const handleApplyFilters = () => {
// Wende lokale Filter auf aktive Filter an
setFilters(prev => ({
const applyFilters = () => {
setFilters((prev) => ({
...prev,
woid: localFilters.woid || undefined,
customer: localFilters.customer || undefined,
@@ -45,380 +33,96 @@ export default function TicketsPage() {
type: localFilters.type ? [localFilters.type] : [],
system: localFilters.system ? [localFilters.system] : [],
priority: localFilters.priority ? [parseInt(localFilters.priority)] : [],
limit: limit
limit,
}))
}
const handleLimitChange = (e) => {
const newLimit = parseInt(e.target.value)
setLimit(newLimit)
// Limit-Änderung wird sofort angewendet (kein Apply nötig)
setFilters(prev => ({ ...prev, limit: newLimit }))
}
const handleUpdate = async (id, data) => {
await updateWorkorder(id, data)
}
const handleUpdate = async (id, data) => { await updateWorkorder(id, data) }
const handleCreate = async (data) => {
const result = await createWorkorder(data)
if (result.success) {
setShowCreateModal(false)
}
if (result.success) setShowCreateModal(false)
return result
}
const handleLoadMore = () => {
setLimit(prev => prev + 10)
setFilters(prev => ({ ...prev, limit: prev.limit + 10 }))
const loadMore = () => {
const next = limit + 10
setLimit(next)
setFilters((prev) => ({ ...prev, limit: next }))
}
return (
<div className="main-content">
{/* Sticky Header Container */}
<div style={{
position: 'sticky',
top: 0,
zIndex: 100,
marginBottom: '24px'
}}>
{/* Compact Control Panel */}
<div style={{
background: 'rgba(26, 32, 44, 0.4)',
backdropFilter: 'blur(25px) saturate(180%)',
WebkitBackdropFilter: 'blur(25px) saturate(180%)',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.3)',
overflow: 'hidden',
padding: '16px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
marginBottom: '16px'
}}>
{/* Title Row */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px',
flexWrap: 'wrap',
gap: '12px'
}}>
<h2 style={{
color: 'var(--dark-text)',
fontSize: '24px',
fontWeight: 'bold',
margin: 0
}}>
Tickets
</h2>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className="btn btn-dark"
onClick={() => setShowCreateModal(true)}
style={{ whiteSpace: 'nowrap' }}
>
CREATE NEW TICKET
</button>
<button
className="btn btn-dark"
onClick={() => setShowOverviewModal(true)}
style={{ whiteSpace: 'nowrap' }}
>
QUICK OVERVIEW
</button>
<button
className="btn btn-green"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
style={{ whiteSpace: 'nowrap' }}
>
{showAdvancedFilters ? '▲ Hide Filters' : '▼ Show Filters'}
</button>
</div>
</div>
<div className="page">
<PageHeader
title="Tickets"
subtitle="Alle Arbeitsauftraege auf einen Blick"
actions={
<>
<Button variant="ghost" onClick={() => setShowOverviewModal(true)}><FaList /> Schnelluebersicht</Button>
<Button variant="ghost" onClick={() => setShowAdvanced((s) => !s)}><FaSliders /> Filter</Button>
<Button variant="primary" onClick={() => setShowCreateModal(true)}><FaPlus /> Neues Ticket</Button>
</>
}
/>
{/* Main Search Bar */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '12px',
alignItems: 'center'
}}>
<input
type="text"
placeholder="WOID"
className="form-control"
style={{ margin: 0 }}
value={localFilters.woid || ''}
onChange={(e) => setLocalFilters({ ...localFilters, woid: e.target.value })}
/>
<input
type="text"
placeholder="Customer"
className="form-control"
style={{ margin: 0 }}
value={localFilters.customer || ''}
onChange={(e) => setLocalFilters({ ...localFilters, customer: e.target.value })}
/>
<input
type="text"
placeholder="User"
className="form-control"
style={{ margin: 0 }}
value={localFilters.userTopic || ''}
onChange={(e) => setLocalFilters({ ...localFilters, userTopic: e.target.value })}
/>
<button
className="btn btn-green"
onClick={handleApplyFilters}
style={{ margin: 0 }}
>
Apply!
</button>
</div>
{/* Advanced Filters - Collapsible */}
{showAdvancedFilters && (
<div style={{
marginTop: '16px',
paddingTop: '16px',
borderTop: '1px solid rgba(16, 185, 129, 0.2)'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '12px',
alignItems: 'center',
marginBottom: '16px'
}}>
<input
type="text"
placeholder="Created Date"
className="form-control"
style={{ margin: 0 }}
value={localFilters.createdDate || ''}
onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })}
/>
<select
className="form-control"
style={{ margin: 0 }}
value={localFilters.type || ''}
onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}
>
<option value="">Type / Location</option>
<option>Home Office</option>
<option>Holidays</option>
<option>Trip</option>
<option>Supportrequest</option>
<option>Change Request</option>
<option>Maintenance</option>
<option>Project</option>
<option>Procurement</option>
<option>Emergency Call</option>
</select>
<select
className="form-control"
style={{ margin: 0 }}
value={localFilters.system || ''}
onChange={(e) => setLocalFilters({ ...localFilters, system: e.target.value })}
>
<option value="">System</option>
<option>Client</option>
<option>Server</option>
<option>Network</option>
<option>EDI</option>
<option>TOS</option>
<option>Reports</option>
<option>n/a</option>
</select>
<select
className="form-control"
style={{ margin: 0 }}
value={localFilters.priority || ''}
onChange={(e) => setLocalFilters({ ...localFilters, priority: e.target.value })}
>
<option value="">Priority</option>
<option value="0">None</option>
<option value="1">Low</option>
<option value="2">Medium</option>
<option value="3">High</option>
<option value="4">Critical</option>
</select>
</div>
{/* Quick Selection Buttons */}
<div style={{
display: 'flex',
gap: '8px',
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'wrap',
marginBottom: '16px'
}}>
<button
className="btn btn-green"
onClick={() => {
setLocalFilters(prev => ({ ...prev, type: 'Procurement' }))
setFilters(prev => ({ ...prev, type: ['Procurement'] }))
setTimeout(() => refresh(), 0)
}}
>
Procurements
</button>
<button
className="btn btn-green"
onClick={() => {
setLocalFilters(prev => ({ ...prev, priority: '4' }))
setFilters(prev => ({ ...prev, priority: [4] }))
setTimeout(() => refresh(), 0)
}}
>
Criticals
</button>
<button
className="btn btn-green"
onClick={() => {
setLocalFilters(prev => ({ ...prev, priority: '3' }))
setFilters(prev => ({ ...prev, priority: [3] }))
setTimeout(() => refresh(), 0)
}}
>
Highs
</button>
<div style={{
width: '1px',
height: '32px',
background: 'rgba(16, 185, 129, 0.3)',
margin: '0 8px'
}}></div>
<button
className="btn btn-green"
onClick={() => {
setLimit(10)
setFilters(prev => ({ ...prev, limit: 10 }))
setTimeout(() => refresh(), 0)
}}
>
10
</button>
<button
className="btn btn-green"
onClick={() => {
setLimit(25)
setFilters(prev => ({ ...prev, limit: 25 }))
setTimeout(() => refresh(), 0)
}}
>
25
</button>
</div>
{/* Slider */}
<div style={{
display: 'flex',
gap: '16px',
alignItems: 'center'
}}>
<span style={{ color: 'var(--dark-text)', minWidth: '120px' }}>
Load Limit: {limit}
</span>
<div style={{ flex: '1' }}>
<input
type="range"
min="5"
max="50"
value={limit}
className="slider"
onChange={handleLimitChange}
/>
</div>
</div>
</div>
)}
<Card pad className="mb-2" style={{ marginBottom: 16 }}>
<div className="toolbar" style={{ marginBottom: showAdvanced ? 16 : 0 }}>
<input className="form-control" style={{ maxWidth: 160 }} placeholder="WOID" value={localFilters.woid} onChange={(e) => setLocalFilters({ ...localFilters, woid: e.target.value })} />
<input className="form-control" style={{ maxWidth: 220 }} placeholder="Kunde" value={localFilters.customer} onChange={(e) => setLocalFilters({ ...localFilters, customer: e.target.value })} />
<input className="form-control search-input" placeholder="Benutzer / Thema" value={localFilters.userTopic} onChange={(e) => setLocalFilters({ ...localFilters, userTopic: e.target.value })} />
<Button variant="primary" onClick={applyFilters}>Anwenden</Button>
</div>
</div>
<table className="table table-hover">
<tbody>
{loading ? (
<tr>
<td colSpan={10} className="text-center p-2">
<FaSpinner className="spinner" size={32} />
<p>Loading...</p>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={10} className="text-center p-2 text-red">
Error: {error}
</td>
</tr>
) : workorders.length === 0 ? (
<tr>
<td colSpan={10} className="text-center p-2">
No tickets found matching your filters.
</td>
</tr>
) : (
workorders.map(ticket => (
<TicketRow
key={ticket.$id}
ticket={ticket}
onUpdate={handleUpdate}
/>
))
)}
</tbody>
</table>
{showAdvanced && (
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 }}>
<input type="date" className="form-control" value={localFilters.createdDate} onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })} />
<select className="form-control" value={localFilters.type} onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}>
<option value="">Typ / Ort</option>
<option>Supportrequest</option><option>Change Request</option><option>Maintenance</option>
<option>Project</option><option>Procurement</option><option>Emergency Call</option>
<option>Home Office</option><option>Holidays</option><option>Trip</option>
</select>
<select className="form-control" value={localFilters.system} onChange={(e) => setLocalFilters({ ...localFilters, system: e.target.value })}>
<option value="">System</option>
<option>Client</option><option>Server</option><option>Network</option>
<option>EDI</option><option>TOS</option><option>Reports</option><option>n/a</option>
</select>
<select className="form-control" value={localFilters.priority} onChange={(e) => setLocalFilters({ ...localFilters, priority: e.target.value })}>
<option value="">Prioritaet</option>
<option value="0">Keine</option><option value="1">Niedrig</option><option value="2">Mittel</option>
<option value="3">Hoch</option><option value="4">Kritisch</option>
</select>
</div>
<div className="flex gap-2 wrap" style={{ marginTop: 12 }}>
<Button variant="ghost" size="sm" onClick={() => { setFilters((p) => ({ ...p, priority: [4] })); setTimeout(refresh, 0) }}>Kritische</Button>
<Button variant="ghost" size="sm" onClick={() => { setFilters((p) => ({ ...p, priority: [3] })); setTimeout(refresh, 0) }}>Hohe</Button>
<Button variant="ghost" size="sm" onClick={() => { setFilters((p) => ({ ...p, type: ['Procurement'] })); setTimeout(refresh, 0) }}>Beschaffung</Button>
</div>
</div>
)}
</Card>
{workorders.length > 0 && workorders.length >= limit && (
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<button
className="btn btn-green"
style={{
padding: '16px 32px',
fontSize: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px',
margin: '0 auto'
}}
onClick={handleLoadMore}
>
Load More <FaAngleDown size={20} />
</button>
{loading ? (
<div className="empty"><FaSpinner className="spinner" /></div>
) : error ? (
<Card><div className="text-red">Fehler: {error}</div></Card>
) : workorders.length === 0 ? (
<Card><EmptyState title="Keine Tickets" hint="Keine Tickets passen zu deinen Filtern." action={<Button variant="primary" onClick={() => setShowCreateModal(true)}><FaPlus /> Neues Ticket</Button>} /></Card>
) : (
<div className="tickets-list">
{workorders.map((ticket) => (
<TicketCard key={ticket.$id} ticket={ticket} onUpdate={handleUpdate} />
))}
</div>
)}
<div style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.2)',
padding: '16px',
marginTop: '24px',
textAlign: 'center',
color: '#a0aec0'
}}>
<p style={{ margin: 0 }}>
Summary: Listed a total of <span style={{
color: 'var(--green-primary)',
fontWeight: 'bold'
}}>{workorders.length}</span> Workorders.
<br />EOL =)
</p>
</div>
{workorders.length > 0 && workorders.length >= limit && (
<div style={{ textAlign: 'center', marginTop: 24 }}>
<Button variant="ghost" onClick={loadMore}>Mehr laden <FaAngleDown /></Button>
</div>
)}
<CreateTicketModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onCreate={handleCreate}
customers={customers}
/>
<QuickOverviewModal
isOpen={showOverviewModal}
onClose={() => setShowOverviewModal(false)}
workorders={workorders}
/>
<CreateTicketModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} onCreate={handleCreate} customers={customers} />
<QuickOverviewModal isOpen={showOverviewModal} onClose={() => setShowOverviewModal(false)} workorders={workorders} />
</div>
)
}