- Work Order Type nur noch Webpage und neu Migration (Kunde kommt von anderem Anbieter) - Affected System, Response Level und Benachrichtigungsfelder entfernt - Service Type: Fernwartung (remote) oder Vor Ort beim Kunden, Standard Fernwartung; gespeichert werden weiter "Remote" / "On Site" - Start und Ende mit Kalender und Uhrzeitfeld; gespeichert weiter als dd.mm.yyyy / hhmm - Requested by/for schlagen Kunden, Mitarbeiter und Namen aus den letzten Tickets vor, die des gewählten Kunden zuerst - Neue Tickets speichern Kundenname und Ort (die Karte zeigte sonst "Unbekannt") - Ticketfilter kennt Webpage und Migration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
9.3 KiB
React
173 lines
9.3 KiB
React
import { useEffect, useState } from 'react'
|
|
import { useSearchParams } from 'react-router-dom'
|
|
import { FaAngleDown, FaSpinner, FaPlus, FaList, FaSliders } from 'react-icons/fa6'
|
|
import { useWorkorders } from '../hooks/useWorkorders'
|
|
import { useCustomers } from '../hooks/useCustomers'
|
|
import TicketCard from '../components/TicketCard'
|
|
import CreateTicketModal from '../components/CreateTicketModal'
|
|
import QuickOverviewModal from '../components/QuickOverviewModal'
|
|
import { PageHeader, Card, Button, EmptyState } from '../components/ui'
|
|
|
|
// Alle Ticket-Status; "Closed" gilt als archiviert und ist standardmaessig ausgeblendet
|
|
const ALL_STATUSES = [
|
|
'Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info',
|
|
'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed',
|
|
]
|
|
const ARCHIVED_STATUSES = ['Closed']
|
|
const DEFAULT_STATUSES = ALL_STATUSES.filter((s) => !ARCHIVED_STATUSES.includes(s))
|
|
|
|
export default function TicketsPage() {
|
|
const [limit, setLimit] = useState(10)
|
|
const [filters, setFilters] = useState({ status: DEFAULT_STATUSES, type: [], priority: [], limit: 10 })
|
|
const [statusSel, setStatusSel] = useState(DEFAULT_STATUSES)
|
|
const [localFilters, setLocalFilters] = useState({
|
|
woid: '', customer: '', userTopic: '', createdDate: '', type: '', system: '', priority: '',
|
|
})
|
|
|
|
const toggleStatus = (status) => {
|
|
setStatusSel((prev) =>
|
|
prev.includes(status) ? prev.filter((s) => s !== status) : [...prev, status]
|
|
)
|
|
}
|
|
|
|
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
|
|
const { customers } = useCustomers()
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
const [searchParams] = useSearchParams()
|
|
|
|
// Deep-Link aus den Leads: /tickets?woid=123 -> Filter vorbelegen + anwenden
|
|
// (ueber ALLE Status, damit das Ticket unabhaengig vom Status gefunden wird)
|
|
useEffect(() => {
|
|
const w = searchParams.get('woid')
|
|
if (!w) return
|
|
setLocalFilters((p) => ({ ...p, woid: w }))
|
|
setShowAdvanced(true)
|
|
setStatusSel(ALL_STATUSES)
|
|
setFilters((prev) => ({ ...prev, status: ALL_STATUSES, woid: w }))
|
|
}, [searchParams])
|
|
|
|
const applyFilters = () => {
|
|
setFilters((prev) => ({
|
|
...prev,
|
|
status: statusSel.length ? statusSel : ALL_STATUSES,
|
|
woid: localFilters.woid || undefined,
|
|
customer: localFilters.customer || undefined,
|
|
userTopic: localFilters.userTopic || undefined,
|
|
createdDate: localFilters.createdDate || undefined,
|
|
type: localFilters.type ? [localFilters.type] : [],
|
|
system: localFilters.system ? [localFilters.system] : [],
|
|
priority: localFilters.priority ? [parseInt(localFilters.priority)] : [],
|
|
limit,
|
|
}))
|
|
}
|
|
|
|
const handleUpdate = async (id, data) => { await updateWorkorder(id, data) }
|
|
const handleCreate = async (data) => {
|
|
const result = await createWorkorder(data)
|
|
if (result.success) setShowCreateModal(false)
|
|
return result
|
|
}
|
|
const loadMore = () => {
|
|
const next = limit + 10
|
|
setLimit(next)
|
|
setFilters((prev) => ({ ...prev, limit: next }))
|
|
}
|
|
|
|
return (
|
|
<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>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<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>
|
|
|
|
{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>Webpage</option><option>Migration</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 style={{ marginTop: 16 }}>
|
|
<div className="flex gap-2 wrap" style={{ alignItems: 'center', marginBottom: 8 }}>
|
|
<span className="muted" style={{ fontSize: 13, fontWeight: 600 }}>Status</span>
|
|
<button type="button" onClick={() => setStatusSel(DEFAULT_STATUSES)} style={{ fontSize: 12, background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', padding: 0 }}>Standard</button>
|
|
<button type="button" onClick={() => setStatusSel(ALL_STATUSES)} style={{ fontSize: 12, background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', padding: 0 }}>Alle inkl. Archiv</button>
|
|
</div>
|
|
<div className="flex gap-3 wrap">
|
|
{ALL_STATUSES.map((status) => (
|
|
<label key={status} className="flex items-center gap-2" style={{ cursor: 'pointer', fontSize: 14 }}>
|
|
<input type="checkbox" checked={statusSel.includes(status)} onChange={() => toggleStatus(status)} />
|
|
{status}
|
|
{ARCHIVED_STATUSES.includes(status) && <span className="faint" style={{ fontSize: 11 }}>(Archiv)</span>}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</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>
|
|
|
|
{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>
|
|
)}
|
|
|
|
{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} />
|
|
</div>
|
|
)
|
|
}
|