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:
1 parent
30b3caee34
commit
ff999ae1b3
5 files changed
+1
-321
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user