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:
1 parent
d86f9c07cd
commit
d6becf0151
6 files changed
+25
-70
No files matched your search
@@ -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' },
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
Reference in new issue
Block a user