Tickets: TicketCard, Dropdowns, statusClass und Altklassen (.tcard, .dropdown, .ticket-tab, .chevron) geloescht

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 06:03:09 +00:00
1 parent 30b3caee34
commit ff999ae1b3
5 files changed
+1 -321

No files matched your search

-37
View File
@@ -1,37 +0,0 @@
import { FaUserPlus } from 'react-icons/fa6'
import { useEmployees } from '../hooks/useEmployees'
export default function EditorDropdown({ value, onChange }) {
const { employees } = useEmployees()
// Finde den zugewiesenen Mitarbeiter anhand der userId
const assignedEmployee = employees.find(emp => emp.userId === value)
// Zeige Kürzel wenn zugewiesen, sonst Icon
const displayValue = assignedEmployee?.shortcode || <FaUserPlus size={20} />
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{displayValue}
</button>
<div className="dropdown-content">
<span
className="dropdown-item"
onClick={() => onChange('')}
>
<em>(Unassigned)</em>
</span>
{employees.map(employee => (
<span
key={employee.$id}
className="dropdown-item"
onClick={() => onChange(employee.userId)}
>
{employee.displayName}{employee.shortcode ? ` (${employee.shortcode})` : ''}
</span>
))}
</div>
</div>
)
}
-22
View File
@@ -1,22 +0,0 @@
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
export default function StatusDropdown({ value, onChange }) {
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{statusLabel(value)}
</button>
<div className="dropdown-content">
{TICKET_STATUSES.map(status => (
<span
key={status.value}
className="dropdown-item"
onClick={() => onChange(status.value)}
>
{status.label}
</span>
))}
</div>
</div>
)
}
-179
View File
@@ -1,179 +0,0 @@
import { useState } from 'react'
import { FaChevronDown, FaPlay, FaStop, FaPlus } from 'react-icons/fa6'
import { formatDistanceToNow, format } from 'date-fns'
import { de } from 'date-fns/locale'
import StatusDropdown from './StatusDropdown'
import UrgentToggle from './UrgentToggle'
import EditorDropdown from './EditorDropdown'
import CreateWorksheetModal from './CreateWorksheetModal'
import WorksheetList from './WorksheetList'
import WorksheetTimesModal from './WorksheetTimesModal'
import OwnSupportModal from './OwnSupportModal'
import WorksheetSummary from './WorksheetSummary'
import TicketProjects from './TicketProjects'
import ProjectContextPanel from './ProjectContextPanel'
import Tabs from './ui/Tabs'
import Badge from './ui/Badge'
import { StatusPill, UrgentPill } from './ui/StatusPill'
import { useWorksheets } from '../hooks/useWorksheets'
import { useCustomerAbo } from '../hooks/useCustomerAbo'
import { useAuth } from '../context/AuthContext'
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
import { canEditWorksheetTimes } from '../lib/ticketForm'
import { canAddOwnSupport } from '../lib/support'
import { ticketTypeLabel } from '../lib/ticketStatus'
// Projekt / Preview steht mit im Details-Reiter
const TABS = [
{ id: 'details', label: 'Details' },
{ id: 'worksheets', label: 'Arbeitsblätter' },
]
export default function TicketCard({ ticket, onUpdate }) {
const { user, employee } = useAuth()
const [expanded, setExpanded] = useState(false)
const [activeTab, setActiveTab] = useState('details')
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
const [ownSupportWorksheet, setOwnSupportWorksheet] = useState(null)
// Abo-Stundenkonto gibt es nur bei Wartungs-Abo; erst laden, wenn Arbeitsblaetter gebraucht werden
const { hoursAllowed: aboHoursAllowed } = useCustomerAbo(
ticket.customerId,
(expanded && activeTab === 'worksheets') || showCreateWorksheet
)
const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheet, updateWorksheetTimes, saveOwnSupport, 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()
// Abo-Stunden: Arbeitszeit vom Stundenguthaben des Wartungs-Abos abziehen oder nicht
const handleTogglePlan = (ws) => updateWorksheet(ws.$id, { countsToPlan: !ws.countsToPlan })
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 })
}
// Akquise-Automatik: Ergebnis im Worksheet steuert den Kundenstatus
if (result.success && ticket.type === 'Akquise' && ticket.customerId) {
const outcome = worksheetData.newStatus
const next = outcome === 'Zugesagt' ? 'customer' : outcome === 'Abgesagt' ? 'lost' : null
if (next) {
try {
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, ticket.customerId, {
customerStatus: next,
updatedAt: new Date().toISOString(),
})
} catch { /* nicht kritisch */ }
}
}
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">· {ticket.customerLocation}</span>}
</div>
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
<div className="tcard-badges">
<StatusPill status={ticket.status} />
<UrgentPill priority={ticket.priority} />
{ticket.type && <Badge tone="muted">{ticketTypeLabel(ticket.type)}</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>
<UrgentToggle value={ticket.priority} onChange={(v) => onUpdate(ticket.$id, { priority: v })} />
<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>
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
<div className="ticket-tab-content">
{activeTab === 'details' && (
<div>
<div className="ui-card pad" style={{ marginBottom: 16 }}>
<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>
<ProjectContextPanel ticket={ticket} />
<TicketProjects ticket={ticket} />
</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 hinzufügen
</button>
</div>
<WorksheetSummary worksheets={worksheets} />
<WorksheetList
worksheets={worksheets}
totalTime={getTotalTime()}
loading={worksheetsLoading}
canEditTimes={(ws) => canEditWorksheetTimes(ws, user, employee)}
onEditTimes={setEditTimesWorksheet}
canAddSupport={(ws) => canAddOwnSupport(ws, user, employee)}
onOwnSupport={setOwnSupportWorksheet}
currentUserId={user?.$id}
onTogglePlan={handleTogglePlan}
aboHoursAllowed={aboHoursAllowed}
/>
</div>
)}
</div>
</div>
)}
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} aboHoursAllowed={aboHoursAllowed} />
{ownSupportWorksheet && (
<OwnSupportModal
worksheet={ownSupportWorksheet}
currentUserId={user?.$id}
onClose={() => setOwnSupportWorksheet(null)}
onSave={(ws, task) => saveOwnSupport(ws, task, user, employee)}
/>
)}
{editTimesWorksheet && (
<WorksheetTimesModal
worksheet={editTimesWorksheet}
currentUserId={user?.$id}
onClose={() => setEditTimesWorksheet(null)}
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
/>
)}
</div>
)
}
-14
View File
@@ -74,20 +74,6 @@ export function statusValuesFor(groupIds) {
return STATUS_GROUPS.filter((group) => groupIds.includes(group.id)).flatMap((group) => group.values)
}
const GROUP_CLASS = {
open: 'status-open',
working: 'status-working',
waiting: 'status-awaiting',
review: 'status-assigned',
done: 'status-closed',
cancelled: 'status-closed',
}
// CSS-Klasse fuer das Status-Abzeichen
export function statusClass(status) {
return GROUP_CLASS[statusGroup(status || 'Open')] || 'status-open'
}
// Statt fuenf Prioritaeten gibt es nur noch "dringend" oder nicht.
// Frueher gespeicherte Stufen: Hoch (3) und Kritisch (4) gelten als dringend.
export const NORMAL_PRIORITY = 2
+1 -69
View File
@@ -523,46 +523,6 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
.page-subtitle { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
.page-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
/* --- Ticket Card (ersetzt Tabelle) --- */
.tickets-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.tcard {
background: var(--surface-1); border: 1px solid var(--border);
border-radius: var(--r-md); overflow: visible; transition: border-color 0.15s ease;
}
.tcard:hover { border-color: var(--border-strong); }
.tcard.open { border-color: var(--border-accent); }
.tcard-detail { border-radius: 0 0 var(--r-md) var(--r-md); }
.tcard-main {
display: grid; grid-template-columns: 92px 1fr auto; gap: var(--sp-4);
padding: var(--sp-4); align-items: center; cursor: pointer;
}
.tcard-woid { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.tcard-woid .id { font-weight: 800; font-size: 15px; color: var(--accent); }
.tcard-woid .age { font-size: 11px; color: var(--text-faint); }
.tcard-body { min-width: 0; }
.tcard-row1 { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: 4px; }
.tcard-customer { font-weight: 700; }
.tcard-loc { color: var(--text-muted); font-size: 13px; }
.tcard-topic { color: var(--text-muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tcard-badges { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
/* Eine Zeile ueber der Arbeitsblatt-Liste */
.worksheet-summary {
display: flex; flex-wrap: wrap; gap: 6px 20px; align-items: baseline;
padding: 10px 14px; margin-bottom: 16px; font-size: 14px; color: var(--text-muted);
background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-md);
}
.worksheet-summary strong { color: var(--text); }
.worksheet-summary .open { color: var(--orange-fg); font-weight: 700; }
.tcard-side { display: flex; align-items: center; gap: var(--sp-3); }
.tcard-detail { border-top: 1px solid var(--border); padding: var(--sp-4); background: var(--surface-1); }
.chevron { color: var(--text-muted); transition: transform 0.2s ease; }
.chevron.up { transform: rotate(180deg); }
@media (max-width: 720px) {
.tcard-main { grid-template-columns: 64px 1fr; }
.tcard-side { grid-column: 1 / -1; justify-content: space-between; }
}
/* --- Generic data list rows (Kunden/Finanzen/Dokumente) --- */
.list { display: flex; flex-direction: column; }
@@ -600,18 +560,6 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
/* Planboard (bis P3): die 7-Spalten-Woche scrollt waagerecht im Container, nicht die Seite */
.planboard-scroll { overflow-x: auto; }
.automatic-assignment-note {
padding: 12px 14px;
border-left: 2px solid var(--accent);
background: var(--surface-2);
}
.automatic-assignment-note p {
margin: 5px 0 0;
color: var(--text-muted);
font-size: 13px;
line-height: 1.45;
}
/* Neues Ticket: Datum + Uhrzeit nebeneinander (heller Kalender, kein color-scheme: dark) */
.datetime-pair { display: flex; gap: 8px; }
.datetime-pair .form-control, .datetime-pair .input { flex: 1; min-width: 0; }
@@ -1134,13 +1082,6 @@ select.form-control {
}
.overlay-close:hover { color: var(--text); background: var(--bg-hover); }
/* Reiter (Tabs.jsx): Unterstrich statt Kasten wie .tabs / .tab */
.ticket-tabs { display: flex; gap: 24px; margin-bottom: 16px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: none; }
.ticket-tab { margin-bottom: -1px; padding: 8px 0 10px; border-bottom: 2px solid transparent; color: var(--text-2); font-weight: 500; white-space: nowrap; }
.ticket-tab:hover { color: var(--text); }
.ticket-tab-active, .ticket-tab-active:hover { border-bottom-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.ticket-tab-content { min-height: 120px; }
/* Umschalter */
.seg button.active { background: var(--bg); color: var(--text); box-shadow: 0 1px 2px rgba(26,31,54,.12), 0 0 0 1px rgba(26,31,54,.04); }
@@ -1156,7 +1097,7 @@ select.form-control {
border-radius: var(--radius); background: var(--grey-bg); color: var(--grey-fg); font-weight: 600;
}
/* Neutral: Karte, Raster, Aufklappmenue */
/* Neutral: Karte, Raster */
.card { margin-bottom: 16px; background: none; border: 0; border-radius: 0; color: var(--text); }
.card-header { padding: 0 0 8px; background: none; border-bottom: 1px solid var(--line); color: var(--text); font-weight: 600; }
.card-body { padding: 12px 0; color: var(--text); }
@@ -1164,15 +1105,6 @@ div.row { display: flex; flex-wrap: wrap; margin: 0 -8px; }
.col { padding: 0 8px; }
.col-6 { width: 50%; }
@media (max-width: 768px) { .col-6 { width: 100%; } }
.dropdown { position: relative; display: inline-block; width: 100%; text-align: center; }
.dropdown .btn { width: 100%; padding: 0 8px; font-size: 12px; box-shadow: none; }
.dropdown-content {
display: none; position: absolute; left: 50%; transform: translateX(-50%); z-index: 100; min-width: 160px;
background: var(--bg); border-radius: var(--radius); box-shadow: var(--shadow-pop); text-align: left;
}
.dropdown:hover .dropdown-content { display: block; }
.dropdown-item { display: block; padding: 8px 16px; color: var(--text); cursor: pointer; }
.dropdown-item:hover { background: var(--bg-hover); }
/* Laden: neutraler grauer Ring */
.spinner { width: 40px; height: 40px; margin: 0 auto; border: 4px solid var(--line); border-top-color: var(--text-2); border-radius: 50%; animation: spin 1s linear infinite; }