Ticketkarte und Admin an das neue Formular angepasst

- Ticketkarte: Response-Level-Auswahl entfernt; Service Type als
  "Fernwartung" / "Vor Ort" (altes "Off Site" zählt als Fernwartung)
- Admin > Konfiguration: Ticketarten, Systeme, Reaktions- und Leistungsarten
  ausgeblendet, da das Formular sie fest vorgibt; editierbar bleiben die
  Prioritäten, die Listen bleiben beim Speichern im Config-Dokument erhalten
- Kaputtes Trennzeichen "�" wieder "·" (Ticketkarte, Kundenliste, Kundendetails)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 19:39:13 +00:00
1 parent d86f9c07cd
commit d6becf0151
6 files changed
+25 -70

No files matched your search

+1 -6
View File
@@ -4,6 +4,7 @@ import { useAdminConfig } from '../hooks/useAdminConfig'
import { useEmployees } from '../hooks/useEmployees' import { useEmployees } from '../hooks/useEmployees'
import { databases, DATABASE_ID, COLLECTIONS, Query, isDemoMode } from '../lib/appwrite' import { databases, DATABASE_ID, COLLECTIONS, Query, isDemoMode } from '../lib/appwrite'
import { import {
SERVICE_TYPES,
todayIso, todayIso,
isoToGermanDate, isoToGermanDate,
timeToWoms, timeToWoms,
@@ -19,12 +20,6 @@ const TICKET_TYPES = [
{ value: 'Migration', label: 'Migration (Übernahme von anderem Anbieter)' }, { value: 'Migration', label: 'Migration (Übernahme von anderem Anbieter)' },
] ]
// Gespeichert werden die bisherigen Werte, angezeigt wird Deutsch
const SERVICE_TYPES = [
{ value: 'Remote', label: 'Fernwartung (remote)' },
{ value: 'On Site', label: 'Vor Ort beim Kunden' },
]
const DEFAULT_PRIORITIES = [ const DEFAULT_PRIORITIES = [
{ value: 0, label: 'None' }, { value: 0, label: 'None' },
{ value: 1, label: 'Low' }, { value: 1, label: 'Low' },
+3 -4
View File
@@ -5,7 +5,6 @@ import { de } from 'date-fns/locale'
import StatusDropdown from './StatusDropdown' import StatusDropdown from './StatusDropdown'
import PriorityDropdown from './PriorityDropdown' import PriorityDropdown from './PriorityDropdown'
import EditorDropdown from './EditorDropdown' import EditorDropdown from './EditorDropdown'
import ResponseDropdown from './ResponseDropdown'
import CreateWorksheetModal from './CreateWorksheetModal' import CreateWorksheetModal from './CreateWorksheetModal'
import StatusHistoryModal from './StatusHistoryModal' import StatusHistoryModal from './StatusHistoryModal'
import WorksheetList from './WorksheetList' import WorksheetList from './WorksheetList'
@@ -21,6 +20,7 @@ import Badge from './ui/Badge'
import { StatusPill, PriorityPill } from './ui/StatusPill' import { StatusPill, PriorityPill } from './ui/StatusPill'
import { useWorksheets } from '../hooks/useWorksheets' import { useWorksheets } from '../hooks/useWorksheets'
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite' import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
import { serviceTypeLabel } from '../lib/ticketForm'
const TABS = [ const TABS = [
{ id: 'details', label: 'Details' }, { id: 'details', label: 'Details' },
@@ -82,7 +82,7 @@ export default function TicketCard({ ticket, onUpdate }) {
<div className="tcard-body"> <div className="tcard-body">
<div className="tcard-row1"> <div className="tcard-row1">
<span className="tcard-customer">{ticket.customerName || 'Unbekannt'}</span> <span className="tcard-customer">{ticket.customerName || 'Unbekannt'}</span>
{ticket.customerLocation && <span className="tcard-loc">� {ticket.customerLocation}</span>} {ticket.customerLocation && <span className="tcard-loc">· {ticket.customerLocation}</span>}
</div> </div>
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div> <div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
<div className="tcard-badges"> <div className="tcard-badges">
@@ -90,7 +90,7 @@ export default function TicketCard({ ticket, onUpdate }) {
<PriorityPill priority={ticket.priority} /> <PriorityPill priority={ticket.priority} />
{ticket.type && <Badge tone="muted">{ticket.type}</Badge>} {ticket.type && <Badge tone="muted">{ticket.type}</Badge>}
{ticket.systemType && ticket.systemType !== 'n/a' && <Badge tone="muted">{ticket.systemType}</Badge>} {ticket.systemType && ticket.systemType !== 'n/a' && <Badge tone="muted">{ticket.systemType}</Badge>}
{ticket.serviceType && <Badge tone="info">{ticket.serviceType}</Badge>} {ticket.serviceType && <Badge tone="info">{serviceTypeLabel(ticket.serviceType)}</Badge>}
</div> </div>
</div> </div>
@@ -113,7 +113,6 @@ export default function TicketCard({ ticket, onUpdate }) {
{ticket.requestedBy && <span className="muted">Angefragt von: {ticket.requestedBy}</span>} {ticket.requestedBy && <span className="muted">Angefragt von: {ticket.requestedBy}</span>}
<span className="grow" /> <span className="grow" />
<div style={{ minWidth: 150 }}><EditorDropdown value={ticket.assignedTo} onChange={(v) => onUpdate(ticket.$id, { assignedTo: v })} /></div> <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> </div>
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} /> <Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
+14 -1
View File
@@ -1,8 +1,21 @@
/* Hilfen fuer "Neues Ticket". /* Hilfen fuer "Neues Ticket" und die Anzeige der Tickets.
WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm" WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm"
(siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen (siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen
"yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */ "yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */
// Service Type: gespeichert werden die bisherigen Werte, angezeigt wird Deutsch
export const SERVICE_TYPES = [
{ value: 'Remote', label: 'Fernwartung (remote)' },
{ value: 'On Site', label: 'Vor Ort beim Kunden' },
]
// Kurzform fuer Badges. Das alte "Off Site" meinte ebenfalls Arbeit aus der Ferne.
const SERVICE_TYPE_SHORT = { remote: 'Fernwartung', 'off site': 'Fernwartung', 'on site': 'Vor Ort' }
export function serviceTypeLabel(value) {
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
}
const pad = (n) => String(n).padStart(2, '0') const pad = (n) => String(n).padStart(2, '0')
// Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">) // Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">)
+3 -55
View File
@@ -16,6 +16,9 @@ import { useCustomers } from '../hooks/useCustomers'
import { useEmployees } from '../hooks/useEmployees' import { useEmployees } from '../hooks/useEmployees'
import './AdminPage.css' import './AdminPage.css'
// Ticketarten, Systeme, Reaktions- und Leistungsarten gibt das Ticketformular
// fest vor; sie sind hier nicht mehr editierbar, bleiben beim Speichern aber
// im Config-Dokument erhalten. Editierbar sind nur noch die Prioritaeten.
const EMPTY_CONFIG = { const EMPTY_CONFIG = {
ticketTypes: [], ticketTypes: [],
systems: [], systems: [],
@@ -41,56 +44,11 @@ const EMPTY_EMPLOYEE = {
isAdmin: false, isAdmin: false,
} }
const CONFIG_GROUPS = [
{ field: 'ticketTypes', title: 'Ticketarten', placeholder: 'Neue Ticketart' },
{ field: 'systems', title: 'Betroffene Systeme', placeholder: 'Neues System' },
{ field: 'responseLevels', title: 'Reaktionsstufen', placeholder: 'Neue Stufe' },
{ field: 'serviceTypes', title: 'Leistungsarten', placeholder: 'Neue Leistungsart' },
]
function Notice({ notice }) { function Notice({ notice }) {
if (!notice?.text) return null if (!notice?.text) return null
return <div className={`admin-notice ${notice.type}`}>{notice.text}</div> return <div className={`admin-notice ${notice.type}`}>{notice.text}</div>
} }
function ConfigList({ title, field, values, placeholder, onAdd, onRemove, onChange }) {
return (
<section className="admin-config-card">
<div className="admin-section-head">
<div>
<h3>{title}</h3>
<span>{values.length} Einträge</span>
</div>
<button type="button" className="admin-icon-button" onClick={() => onAdd(field)}>
<FaPlus />
<span>Hinzufügen</span>
</button>
</div>
<div className="admin-config-list">
{values.length === 0 && <p className="admin-empty-line">Noch keine Einträge.</p>}
{values.map((value, index) => (
<div className="admin-config-row" key={`${field}-${index}`}>
<input
className="form-control"
value={value}
placeholder={placeholder}
onChange={(event) => onChange(field, index, event.target.value)}
/>
<button
type="button"
className="admin-icon-button danger"
onClick={() => onRemove(field, index)}
aria-label={`${value || 'Eintrag'} entfernen`}
>
<FaTrash />
</button>
</div>
))}
</div>
</section>
)
}
export default function AdminPage() { export default function AdminPage() {
const { user, isAdmin } = useAuth() const { user, isAdmin } = useAuth()
const { config, loading, error, updateConfig } = useAdminConfig() const { config, loading, error, updateConfig } = useAdminConfig()
@@ -338,16 +296,6 @@ export default function AdminPage() {
) : ( ) : (
<> <>
<div className="admin-config-grid"> <div className="admin-config-grid">
{CONFIG_GROUPS.map((group) => (
<ConfigList
key={group.field}
{...group}
values={localConfig[group.field] || []}
onAdd={addConfigItem}
onRemove={removeConfigItem}
onChange={updateConfigItem}
/>
))}
<section className="admin-config-card priorities"> <section className="admin-config-card priorities">
<div className="admin-section-head"> <div className="admin-section-head">
<div><h3>Prioritäten</h3><span>{localConfig.priorities?.length || 0} Einträge</span></div> <div><h3>Prioritäten</h3><span>{localConfig.priorities?.length || 0} Einträge</span></div>
+2 -2
View File
@@ -68,7 +68,7 @@ export default function CustomerDetailPage() {
{archived && <Badge tone="muted"><FaBoxArchive /> Archiviert</Badge>} {archived && <Badge tone="muted"><FaBoxArchive /> Archiviert</Badge>}
</span> </span>
} }
subtitle={[customer.location, customer.email, customer.phone].filter(Boolean).join(' � ')} subtitle={[customer.location, customer.email, customer.phone].filter(Boolean).join(' · ')}
actions={ actions={
<> <>
<Button variant="ghost" onClick={toggleArchived}> <Button variant="ghost" onClick={toggleArchived}>
@@ -305,7 +305,7 @@ function CustomerTickets({ customerId }) {
<span className="mono" style={{ color: 'var(--accent)', minWidth: 60 }}>{t.woid || t.$id.slice(-5)}</span> <span className="mono" style={{ color: 'var(--accent)', minWidth: 60 }}>{t.woid || t.$id.slice(-5)}</span>
<div className="grow"> <div className="grow">
<div style={{ fontWeight: 600 }}>{t.topic || t.title || '-'}</div> <div style={{ fontWeight: 600 }}>{t.topic || t.title || '-'}</div>
<div className="muted">{t.type} � {t.systemType || 'n/a'}</div> <div className="muted">{t.type} · {t.systemType || 'n/a'}</div>
</div> </div>
<PriorityPill priority={t.priority} /> <PriorityPill priority={t.priority} />
<StatusPill status={t.status} /> <StatusPill status={t.status} />
+2 -2
View File
@@ -82,9 +82,9 @@ export default function CustomersPage() {
<div style={{ fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 }}>
{c.importantCustomer && <FaStar style={{ color: 'var(--warn)' }} title="Wichtiger Kunde" />} {c.importantCustomer && <FaStar style={{ color: 'var(--warn)' }} title="Wichtiger Kunde" />}
{c.name} {c.name}
{c.code && <span className="faint" style={{ fontWeight: 400 }}>� {c.code}</span>} {c.code && <span className="faint" style={{ fontWeight: 400 }}>· {c.code}</span>}
</div> </div>
<div className="muted">{[c.location, c.email].filter(Boolean).join(' � ') || '-'}</div> <div className="muted">{[c.location, c.email].filter(Boolean).join(' · ') || '-'}</div>
</div> </div>
<div className="flex gap-2 wrap" style={{ justifyContent: 'flex-end' }}> <div className="flex gap-2 wrap" style={{ justifyContent: 'flex-end' }}>
{isArchived(c) && <Badge tone="muted">Archiviert</Badge>} {isArchived(c) && <Badge tone="muted">Archiviert</Badge>}