- 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>
153 lines
7.4 KiB
JavaScript
153 lines
7.4 KiB
JavaScript
import { useState } from 'react'
|
||
import { FaChevronDown, FaPlay, FaStop, FaPlus, FaClockRotateLeft, FaPen } from 'react-icons/fa6'
|
||
import { formatDistanceToNow, format } from 'date-fns'
|
||
import { de } from 'date-fns/locale'
|
||
import StatusDropdown from './StatusDropdown'
|
||
import PriorityDropdown from './PriorityDropdown'
|
||
import EditorDropdown from './EditorDropdown'
|
||
import ResponseDropdown from './ResponseDropdown'
|
||
import CreateWorksheetModal from './CreateWorksheetModal'
|
||
import StatusHistoryModal from './StatusHistoryModal'
|
||
import WorksheetList from './WorksheetList'
|
||
import WorksheetStats from './WorksheetStats'
|
||
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
|
||
import TicketProjectsModal from './TicketProjectsModal'
|
||
import InvoicePanel from './InvoicePanel'
|
||
import DocumentsPanel from './DocumentsPanel'
|
||
import Tabs from './ui/Tabs'
|
||
import Badge from './ui/Badge'
|
||
import { StatusPill, PriorityPill } from './ui/StatusPill'
|
||
import { useWorksheets } from '../hooks/useWorksheets'
|
||
|
||
const TABS = [
|
||
{ id: 'details', label: 'Details' },
|
||
{ id: 'worksheets', label: 'Arbeitsblaetter' },
|
||
{ id: 'invoices', label: 'Rechnungen' },
|
||
{ id: 'documents', label: 'Dokumente' },
|
||
{ id: 'project', label: 'Projekt / Preview' },
|
||
]
|
||
|
||
export default function TicketCard({ ticket, onUpdate }) {
|
||
const [expanded, setExpanded] = useState(false)
|
||
const [activeTab, setActiveTab] = useState('details')
|
||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||
const [showProjectsModal, setShowProjectsModal] = useState(false)
|
||
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
|
||
|
||
const { worksheets, loading: worksheetsLoading, createWorksheet, getTotalTime } = useWorksheets(
|
||
expanded ? ticket.woid : null
|
||
)
|
||
|
||
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
|
||
const elapsed = formatDistanceToNow(createdAt, { locale: de, addSuffix: true })
|
||
|
||
const stop = (e) => e.stopPropagation()
|
||
|
||
const handleCreateWorksheet = async (worksheetData, currentUser) => {
|
||
const result = await createWorksheet(worksheetData, currentUser)
|
||
if (result.success && worksheetData.newStatus !== ticket.status) {
|
||
await onUpdate(ticket.$id, {
|
||
status: worksheetData.newStatus,
|
||
responseLevel: worksheetData.newResponseLevel || ticket.responseLevel,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
return (
|
||
<div className={`tcard ${expanded ? 'open' : ''}`}>
|
||
<div className="tcard-main" onClick={() => setExpanded((e) => !e)}>
|
||
<div className="tcard-woid">
|
||
<span className="id">{ticket.woid || ticket.$id?.slice(-5)}</span>
|
||
<span className="age">{elapsed}</span>
|
||
</div>
|
||
|
||
<div className="tcard-body">
|
||
<div className="tcard-row1">
|
||
<span className="tcard-customer">{ticket.customerName || 'Unbekannt'}</span>
|
||
{ticket.customerLocation && <span className="tcard-loc"><EFBFBD> {ticket.customerLocation}</span>}
|
||
</div>
|
||
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
|
||
<div className="tcard-badges">
|
||
<StatusPill status={ticket.status} />
|
||
<PriorityPill priority={ticket.priority} />
|
||
{ticket.type && <Badge tone="muted">{ticket.type}</Badge>}
|
||
{ticket.systemType && ticket.systemType !== 'n/a' && <Badge tone="muted">{ticket.systemType}</Badge>}
|
||
{ticket.serviceType && <Badge tone="info">{ticket.serviceType}</Badge>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tcard-side" onClick={stop}>
|
||
<div style={{ minWidth: 120 }}>
|
||
<StatusDropdown value={ticket.status} onChange={(v) => onUpdate(ticket.$id, { status: v })} />
|
||
</div>
|
||
<div style={{ minWidth: 110 }}>
|
||
<PriorityDropdown value={ticket.priority} onChange={(v) => onUpdate(ticket.$id, { priority: v })} />
|
||
</div>
|
||
<FaChevronDown className={`chevron ${expanded ? 'up' : ''}`} onClick={() => setExpanded((e) => !e)} style={{ cursor: 'pointer' }} />
|
||
</div>
|
||
</div>
|
||
|
||
{expanded && (
|
||
<div className="tcard-detail">
|
||
<div className="flex gap-3 wrap" style={{ marginBottom: 16, fontSize: 13 }}>
|
||
<span className="muted"><FaPlay style={{ color: 'var(--ok)' }} /> {ticket.startDate || format(createdAt, 'dd.MM.yyyy')}</span>
|
||
<span className="muted"><FaStop style={{ color: 'var(--danger)' }} /> {ticket.deadline || '-'}</span>
|
||
{ticket.requestedBy && <span className="muted">Angefragt von: {ticket.requestedBy}</span>}
|
||
<span className="grow" />
|
||
<div style={{ minWidth: 150 }}><EditorDropdown value={ticket.assignedTo} onChange={(v) => onUpdate(ticket.$id, { assignedTo: v })} /></div>
|
||
<div style={{ minWidth: 150 }}><ResponseDropdown value={ticket.responseLevel} onChange={(v) => onUpdate(ticket.$id, { responseLevel: v })} /></div>
|
||
</div>
|
||
|
||
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
|
||
|
||
<div className="ticket-tab-content">
|
||
{activeTab === 'details' && (
|
||
<div className="ui-card pad">
|
||
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Ticket-Beschreibung</h5>
|
||
<p style={{ whiteSpace: 'pre-wrap', color: 'var(--text-muted)', lineHeight: 1.7, margin: 0 }}>
|
||
{ticket.details || 'Keine Details vorhanden.'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'worksheets' && (
|
||
<div>
|
||
<div className="flex gap-2" style={{ marginBottom: 16 }}>
|
||
<button className="ui-btn ui-btn-primary grow" onClick={() => setShowCreateWorksheet(true)}>
|
||
<FaPlus /> Arbeitsblatt hinzufuegen
|
||
</button>
|
||
<button className="ui-btn" onClick={() => setShowProjectsModal(true)} title="Projekte"><FaPen /></button>
|
||
<button className="ui-btn" onClick={() => setShowHistoryModal(true)} title="Verlauf"><FaClockRotateLeft /></button>
|
||
</div>
|
||
{worksheets.length > 0 && (
|
||
<div className="ui-card pad" style={{ marginBottom: 16 }}>
|
||
<WorksheetStats worksheets={worksheets} />
|
||
</div>
|
||
)}
|
||
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} />
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'invoices' && <InvoicePanel ticket={ticket} />}
|
||
{activeTab === 'documents' && <DocumentsPanel ticket={ticket} />}
|
||
{activeTab === 'project' && (
|
||
<div>
|
||
<button className="ui-btn ui-btn-primary" style={{ marginBottom: 12 }} onClick={() => setShowProjectsModal(true)}>
|
||
<FaPen /> Projekte zuweisen / bearbeiten
|
||
</button>
|
||
<TicketAssignedProjectsPanel ticket={ticket} refreshKey={projectsRefreshKey} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} />
|
||
<StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} />
|
||
<TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} />
|
||
</div>
|
||
)
|
||
}
|