Tickets auf WEBklar zugeschnitten (Paket B)

- Status: 6 statt 10, auf Deutsch (Offen, In Arbeit, Wartet auf Kunde,
  Prüfung, Erledigt, Storniert). Gespeichert werden die Werte, die das
  Kundenportal für die Fortschrittsleiste kennt; alte Werte wie Assigned
  werden weiter angezeigt ("Eingeplant"), sind aber nicht mehr wählbar.
- Zuweisen ändert den Status nicht mehr; neue Tickets starten "Offen".
- Priorität: Schalter "Dringend" statt fünf Stufen (dringend = 4, normal
  = 2; alte Stufen Hoch/Kritisch gelten als dringend), rotes Abzeichen.
- Ticketliste: Standardansicht "Aufträge" ohne Akquise-Tickets; Filter
  Typ (Aufträge/Webpage/Migration/Projekt/Akquise/Alle), 6 Status-Gruppen,
  "Nur dringende". Die toten Felder Kunde, Benutzer/Thema und Datum sind
  durch eine echte Suche ersetzt (Kunde, Betreff, Angefragt von, WOID).
  Deep-Link ?woid= zeigt das Ticket weiter unabhängig von Art und Status.
- Formulare "Neues Ticket" und "Arbeitsblatt" auf Deutsch; raus sind
  Workload Estimate, Requested for und die Service-Art am Ticket
  (gespeichert wird weiter Fernwartung).
- Ticketkarte: Projekt/Preview im Details-Reiter (4 statt 5 Reiter), eine
  Zusammenfassungszeile statt der großen Statistik, doppelter
  Projekte-Stift weg, Service-Abzeichen weg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 15:46:03 +00:00
1 parent bf007df737
commit 062d992b63
17 files changed
+359 -626

No files matched your search

+74 -71
View File
@@ -6,59 +6,61 @@ import { useCustomers } from '../hooks/useCustomers'
import TicketCard from '../components/TicketCard'
import CreateTicketModal from '../components/CreateTicketModal'
import { PageHeader, Card, Button, EmptyState } from '../components/ui'
import { STATUS_GROUPS, DEFAULT_STATUS_GROUPS, statusValuesFor, TYPE_FILTERS } from '../lib/ticketStatus'
// 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))
const ALL_GROUPS = STATUS_GROUPS.map((group) => group.id)
const linkButton = { fontSize: 12, background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', padding: 0 }
// Aus Suchtext, Art, Status-Gruppen und "Nur dringende" die Abfrage fuer useWorkorders bauen.
// Sind alle oder keine Status-Gruppen gewaehlt, wird nicht nach Status gefiltert.
function toFilters({ search, typeId, groups, urgentOnly }) {
const typeFilter = TYPE_FILTERS.find((filter) => filter.id === typeId) || TYPE_FILTERS[0]
const allGroups = groups.length === 0 || groups.length === ALL_GROUPS.length
return {
status: allGroups ? [] : statusValuesFor(groups),
type: typeFilter.types || [],
excludeType: typeFilter.excludeType,
priority: urgentOnly ? [3, 4] : [],
search: search.trim() || undefined,
woid: undefined,
}
}
const DEFAULT_SELECTION = { search: '', typeId: 'orders', groups: DEFAULT_STATUS_GROUPS, urgentOnly: false }
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: '', priority: '',
})
const [filters, setFilters] = useState({ ...toFilters(DEFAULT_SELECTION), limit: 10 })
const [searchText, setSearchText] = useState('')
const [typeId, setTypeId] = useState(DEFAULT_SELECTION.typeId)
const [groups, setGroups] = useState(DEFAULT_SELECTION.groups)
const [urgentOnly, setUrgentOnly] = useState(false)
const toggleStatus = (status) => {
setStatusSel((prev) =>
prev.includes(status) ? prev.filter((s) => s !== status) : [...prev, status]
)
const toggleGroup = (id) => {
setGroups((prev) => (prev.includes(id) ? prev.filter((g) => g !== id) : [...prev, id]))
}
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
const { workorders, loading, error, updateWorkorder, createWorkorder } = useWorkorders(filters)
const { customers } = useCustomers()
const [showCreateModal, setShowCreateModal] = 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)
// Deep-Link aus den Leads: /tickets?woid=123 zeigt genau dieses Ticket,
// egal welche Art (auch Akquise) und welcher Status
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 }))
setSearchText(w)
setTypeId('all')
setGroups(ALL_GROUPS)
setFilters((prev) => ({ ...prev, status: [], type: [], excludeType: undefined, priority: [], search: undefined, 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] : [],
priority: localFilters.priority ? [parseInt(localFilters.priority)] : [],
limit,
}))
const applyFilters = (e) => {
e?.preventDefault()
setFilters((prev) => ({ ...prev, ...toFilters({ search: searchText, typeId, groups, urgentOnly }), limit }))
}
const handleUpdate = async (id, data) => { await updateWorkorder(id, data) }
@@ -77,7 +79,7 @@ export default function TicketsPage() {
<div className="page">
<PageHeader
title="Tickets"
subtitle="Alle Arbeitsauftraege auf einen Blick"
subtitle="Alle Arbeitsaufträge auf einen Blick"
actions={
<>
<Button variant="ghost" onClick={() => setShowAdvanced((s) => !s)}><FaSliders /> Filter</Button>
@@ -87,51 +89,45 @@ export default function TicketsPage() {
/>
<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>
<form className="toolbar" onSubmit={applyFilters} style={{ marginBottom: showAdvanced ? 16 : 0 }}>
<input
type="search"
className="form-control search-input"
placeholder="Suchen: Kunde, Betreff, Angefragt von oder WOID"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<Button variant="primary" type="submit">Anwenden</Button>
</form>
{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.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>
<div className="flex gap-3 wrap" style={{ alignItems: 'center' }}>
<select aria-label="Typ" className="form-control" style={{ maxWidth: 220 }} value={typeId} onChange={(e) => setTypeId(e.target.value)}>
{TYPE_FILTERS.map((filter) => (
<option key={filter.id} value={filter.id}>{filter.label}</option>
))}
</select>
<label className="flex items-center gap-2" style={{ cursor: 'pointer', fontSize: 14 }}>
<input type="checkbox" checked={urgentOnly} onChange={(e) => setUrgentOnly(e.target.checked)} />
Nur dringende
</label>
</div>
<div style={{ marginTop: 16 }}>
<div className="flex gap-2 wrap" style={{ alignItems: 'center', marginBottom: 8 }}>
<div className="flex gap-2 wrap status-filter-actions" 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>
<button type="button" onClick={() => setGroups(DEFAULT_STATUS_GROUPS)} style={linkButton}>Standard</button>
<button type="button" onClick={() => setGroups(ALL_GROUPS)} style={linkButton}>Alle</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>}
<div className="flex gap-3 wrap status-filter">
{STATUS_GROUPS.map((group) => (
<label key={group.id} className="flex items-center gap-2" style={{ cursor: 'pointer', fontSize: 14 }}>
<input type="checkbox" checked={groups.includes(group.id)} onChange={() => toggleGroup(group.id)} />
{group.label}
</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>
@@ -141,7 +137,13 @@ export default function TicketsPage() {
) : 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>
<Card>
<EmptyState
title="Keine Tickets"
hint="Keine Tickets passen zu Suche und Filter. Erledigte, stornierte und Akquise-Tickets sind in der Standardansicht ausgeblendet."
action={<Button variant="primary" onClick={() => setShowCreateModal(true)}><FaPlus /> Neues Ticket</Button>}
/>
</Card>
) : (
<div className="tickets-list">
{workorders.map((ticket) => (
@@ -150,7 +152,8 @@ export default function TicketsPage() {
</div>
)}
{workorders.length > 0 && workorders.length >= limit && (
{/* Bei einer Suche kommen alle Treffer auf einmal */}
{!filters.search && workorders.length > 0 && workorders.length >= limit && (
<div style={{ textAlign: 'center', marginTop: 24 }}>
<Button variant="ghost" onClick={loadMore}>Mehr laden <FaAngleDown /></Button>
</div>