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:
1 parent
bf007df737
commit
062d992b63
17 files changed
+359
-626
No files matched your search
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { useAdminConfig } from '../hooks/useAdminConfig'
|
||||
import { useEmployees } from '../hooks/useEmployees'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import {
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
buildRequesterSuggestions,
|
||||
rankSuggestions,
|
||||
} from '../lib/ticketForm'
|
||||
import { priorityFor } from '../lib/ticketStatus'
|
||||
import { AutocompleteInput, DateTimeInput } from './ui'
|
||||
|
||||
// Neue Tickets gibt es aktuell nur fuer diese Arten; alte Tickets behalten ihre Art.
|
||||
@@ -20,36 +20,25 @@ const TICKET_TYPES = [
|
||||
{ value: 'Migration', label: 'Migration (Übernahme von anderem Anbieter)' },
|
||||
]
|
||||
|
||||
const DEFAULT_PRIORITIES = [
|
||||
{ value: 0, label: 'None' },
|
||||
{ value: 1, label: 'Low' },
|
||||
{ value: 2, label: 'Medium' },
|
||||
{ value: 3, label: 'High' },
|
||||
{ value: 4, label: 'Critical' }
|
||||
]
|
||||
|
||||
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder)
|
||||
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder).
|
||||
// Die Service-Art fragt das Formular nicht mehr ab: gespeichert wird Fernwartung.
|
||||
const emptyForm = () => ({
|
||||
customerId: '',
|
||||
type: TICKET_TYPES[0].value,
|
||||
serviceType: SERVICE_TYPES[0].value,
|
||||
priority: 1,
|
||||
urgent: false,
|
||||
topic: '',
|
||||
requestedBy: '',
|
||||
requestedFor: '',
|
||||
startDate: todayIso(),
|
||||
startTime: '',
|
||||
deadline: todayIso(),
|
||||
endTime: '',
|
||||
estimate: '30',
|
||||
details: ''
|
||||
})
|
||||
|
||||
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
||||
const { config } = useAdminConfig()
|
||||
const { employees } = useEmployees()
|
||||
|
||||
const PRIORITIES = config?.priorities || DEFAULT_PRIORITIES
|
||||
const [formData, setFormData] = useState(emptyForm)
|
||||
const [recentTickets, setRecentTickets] = useState([])
|
||||
|
||||
@@ -104,9 +93,11 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
setError('')
|
||||
|
||||
const customer = customers.find(c => c.$id === formData.customerId)
|
||||
const { urgent, ...fields } = formData
|
||||
try {
|
||||
const result = await onCreate({
|
||||
...formData,
|
||||
...fields,
|
||||
priority: priorityFor(urgent),
|
||||
customerName: customer?.name || '',
|
||||
customerLocation: customer?.location || '',
|
||||
startDate: isoToGermanDate(formData.startDate),
|
||||
@@ -136,7 +127,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
<FaTimes />
|
||||
</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Create New Ticket</h2>
|
||||
<h2 className="mb-2">Neues Ticket</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
@@ -148,14 +139,14 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
<div className="row">
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Customer ID</label>
|
||||
<label className="form-label">Kunde</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.customerId}
|
||||
onChange={(e) => handleChange('customerId', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Affected Customer</option>
|
||||
<option value="">Kunde wählen</option>
|
||||
{customers.map(c => (
|
||||
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
|
||||
))}
|
||||
@@ -163,7 +154,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Work Order Type</label>
|
||||
<label className="form-label">Art</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.type}
|
||||
@@ -176,31 +167,14 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Service Type</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.serviceType}
|
||||
onChange={(e) => handleChange('serviceType', e.target.value)}
|
||||
>
|
||||
{SERVICE_TYPES.map(type => (
|
||||
<option key={type.value} value={type.value}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Priority</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.priority}
|
||||
onChange={(e) => handleChange('priority', parseInt(e.target.value))}
|
||||
required
|
||||
>
|
||||
<option value="">Priority Level</option>
|
||||
{PRIORITIES.map(p => (
|
||||
<option key={p.value} value={p.value}>{p.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.urgent}
|
||||
onChange={(e) => handleChange('urgent', e.target.checked)}
|
||||
/>
|
||||
Dringend
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group automatic-assignment-note">
|
||||
@@ -215,11 +189,11 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Topic</label>
|
||||
<label className="form-label">Betreff</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Topic"
|
||||
placeholder="Worum geht es?"
|
||||
value={formData.topic}
|
||||
onChange={(e) => handleChange('topic', e.target.value)}
|
||||
required
|
||||
@@ -227,7 +201,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="ticket-requested-by">Requested by</label>
|
||||
<label className="form-label" htmlFor="ticket-requested-by">Angefragt von</label>
|
||||
<AutocompleteInput
|
||||
id="ticket-requested-by"
|
||||
placeholder="Name"
|
||||
@@ -239,21 +213,10 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="ticket-requested-for">Requested for</label>
|
||||
<AutocompleteInput
|
||||
id="ticket-requested-for"
|
||||
placeholder="Name"
|
||||
value={formData.requestedFor}
|
||||
onChange={(value) => handleChange('requestedFor', value)}
|
||||
getSuggestions={getSuggestions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="ticket-start-date">Start Date & Time</label>
|
||||
<label className="form-label" htmlFor="ticket-start-date">Beginn</label>
|
||||
<DateTimeInput
|
||||
id="ticket-start-date"
|
||||
timeLabel="Start time"
|
||||
timeLabel="Uhrzeit Beginn"
|
||||
date={formData.startDate}
|
||||
time={formData.startTime}
|
||||
onDateChange={handleStartDateChange}
|
||||
@@ -262,10 +225,10 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="ticket-end-date">End Date & Time (Deadline)</label>
|
||||
<label className="form-label" htmlFor="ticket-end-date">Deadline</label>
|
||||
<DateTimeInput
|
||||
id="ticket-end-date"
|
||||
timeLabel="End time"
|
||||
timeLabel="Uhrzeit Deadline"
|
||||
min={formData.startDate}
|
||||
date={formData.deadline}
|
||||
time={formData.endTime}
|
||||
@@ -273,26 +236,15 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
onTimeChange={(value) => handleChange('endTime', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Workload Estimate</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="xx minutes"
|
||||
value={formData.estimate}
|
||||
onChange={(e) => handleChange('estimate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Details</label>
|
||||
<label className="form-label">Beschreibung</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={5}
|
||||
placeholder="Details"
|
||||
placeholder="Was soll gemacht werden?"
|
||||
value={formData.details}
|
||||
onChange={(e) => handleChange('details', e.target.value)}
|
||||
/>
|
||||
@@ -304,7 +256,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
||||
className="btn btn-dark"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating...' : 'CREATE NOW'}
|
||||
{loading ? 'Wird angelegt …' : 'Ticket anlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,24 +2,18 @@ import { useState, useEffect } from 'react'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm'
|
||||
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||||
import { DateTimeInput } from './ui'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
'Open',
|
||||
'Closed',
|
||||
'Awaiting',
|
||||
'Added Info',
|
||||
'Occupied',
|
||||
'Halted',
|
||||
'Cancelled',
|
||||
'Aborted',
|
||||
'Assigned',
|
||||
'In Test'
|
||||
]
|
||||
|
||||
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
||||
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
||||
|
||||
// Die sechs Status; ein alter Status (z. B. "Assigned") bleibt als aktuelle Auswahl sichtbar
|
||||
function statusOptionsFor(current) {
|
||||
if (!current || TICKET_STATUSES.some((s) => s.value === current)) return TICKET_STATUSES
|
||||
return [{ value: current, label: `${statusLabel(current)} (bisher)` }, ...TICKET_STATUSES]
|
||||
}
|
||||
|
||||
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder).
|
||||
// Die Service-Art kommt vom Ticket; das alte "Off Site" zaehlt als Fernwartung.
|
||||
const initialForm = (workorder, newStatus) => ({
|
||||
@@ -41,7 +35,9 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
const { user } = useAuth()
|
||||
|
||||
const isAcquisition = workorder?.type === 'Akquise'
|
||||
const statusOptions = isAcquisition ? ACQUISITION_STATUS : STATUS_OPTIONS
|
||||
const statusOptions = isAcquisition
|
||||
? ACQUISITION_STATUS.map((s) => ({ value: s, label: s }))
|
||||
: statusOptionsFor(workorder?.status)
|
||||
const defaultStatus = isAcquisition
|
||||
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
||||
: (workorder?.status || 'Open')
|
||||
@@ -122,7 +118,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
if (result.success) {
|
||||
onClose()
|
||||
} else {
|
||||
setError(result.error || 'Fehler beim Erstellen des Worksheets')
|
||||
setError(result.error || 'Fehler beim Speichern des Arbeitsblatts')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating worksheet:', err)
|
||||
@@ -140,7 +136,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
<FaTimes />
|
||||
</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
||||
<h2 className="mb-2">Neues Arbeitsblatt – Ticket {workorder.woid}</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
@@ -152,7 +148,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
<div className="row">
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Service Type</label>
|
||||
<label className="form-label">Art</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.serviceType}
|
||||
@@ -166,7 +162,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">{isAcquisition ? 'Akquise-Ergebnis' : 'New Status'}</label>
|
||||
<label className="form-label">{isAcquisition ? 'Akquise-Ergebnis' : 'Neuer Status'}</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.newStatus}
|
||||
@@ -174,7 +170,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
required
|
||||
>
|
||||
{statusOptions.map(status => (
|
||||
<option key={status} value={status}>{status}</option>
|
||||
<option key={status.value} value={status.value}>{status.label}</option>
|
||||
))}
|
||||
</select>
|
||||
{isAcquisition && (
|
||||
@@ -210,10 +206,10 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="worksheet-start-date">Start Date & Time</label>
|
||||
<label className="form-label" htmlFor="worksheet-start-date">Beginn</label>
|
||||
<DateTimeInput
|
||||
id="worksheet-start-date"
|
||||
timeLabel="Start time"
|
||||
timeLabel="Uhrzeit Beginn"
|
||||
required
|
||||
date={formData.startDate}
|
||||
time={formData.startTime}
|
||||
@@ -223,10 +219,10 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="worksheet-end-date">End Date & Time</label>
|
||||
<label className="form-label" htmlFor="worksheet-end-date">Ende</label>
|
||||
<DateTimeInput
|
||||
id="worksheet-end-date"
|
||||
timeLabel="End time"
|
||||
timeLabel="Uhrzeit Ende"
|
||||
required
|
||||
min={formData.startDate}
|
||||
date={formData.endDate}
|
||||
@@ -237,7 +233,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Total Time (Minuten)</label>
|
||||
<label className="form-label">Arbeitszeit (Minuten)</label>
|
||||
{/* Ganze Minuten: mit step 15 blockierte der Browser berechnete Zeiten wie 70 */}
|
||||
<input
|
||||
type="number"
|
||||
@@ -259,7 +255,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Action Details</label>
|
||||
<label className="form-label">Was wurde gemacht?</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={5}
|
||||
@@ -276,7 +272,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
className="btn btn-dark"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating...' : 'CREATE NOW'}
|
||||
{loading ? 'Speichert …' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
const PRIORITIES = [
|
||||
{ value: 0, label: 'None' },
|
||||
{ value: 1, label: 'Low' },
|
||||
{ value: 2, label: 'Medium' },
|
||||
{ value: 3, label: 'High' },
|
||||
{ value: 4, label: 'Critical' }
|
||||
]
|
||||
|
||||
const PRIORITY_LABELS = {
|
||||
0: 'NONE',
|
||||
1: 'LOW',
|
||||
2: 'MEDIUM',
|
||||
3: 'HIGH',
|
||||
4: 'CRITICAL'
|
||||
}
|
||||
|
||||
export default function PriorityDropdown({ value, onChange }) {
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
|
||||
{PRIORITY_LABELS[value] || 'LOW'}
|
||||
</button>
|
||||
<div className="dropdown-content">
|
||||
{PRIORITIES.map(priority => (
|
||||
<span
|
||||
key={priority.value}
|
||||
className="dropdown-item"
|
||||
onClick={() => onChange(priority.value)}
|
||||
>
|
||||
{priority.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +1,19 @@
|
||||
const STATUSES = [
|
||||
'Open',
|
||||
'Occupied',
|
||||
'Assigned',
|
||||
'Awaiting',
|
||||
'Added Info',
|
||||
'In Test',
|
||||
'Halted',
|
||||
'Aborted',
|
||||
'Cancelled',
|
||||
'Closed'
|
||||
]
|
||||
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' }}>
|
||||
{value?.toUpperCase() || 'OPEN'}
|
||||
{statusLabel(value)}
|
||||
</button>
|
||||
<div className="dropdown-content">
|
||||
{STATUSES.map(status => (
|
||||
<span
|
||||
key={status}
|
||||
{TICKET_STATUSES.map(status => (
|
||||
<span
|
||||
key={status.value}
|
||||
className="dropdown-item"
|
||||
onClick={() => onChange(status)}
|
||||
onClick={() => onChange(status.value)}
|
||||
>
|
||||
{status}
|
||||
{status.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@ import { FaChevronDown, FaPlay, FaStop, FaPlus, FaPen } from 'react-icons/fa6'
|
||||
import { formatDistanceToNow, format } from 'date-fns'
|
||||
import { de } from 'date-fns/locale'
|
||||
import StatusDropdown from './StatusDropdown'
|
||||
import PriorityDropdown from './PriorityDropdown'
|
||||
import UrgentToggle from './UrgentToggle'
|
||||
import EditorDropdown from './EditorDropdown'
|
||||
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||
import WorksheetList from './WorksheetList'
|
||||
import WorksheetTimesModal from './WorksheetTimesModal'
|
||||
import WorksheetStats from './WorksheetStats'
|
||||
import WorksheetSummary from './WorksheetSummary'
|
||||
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
|
||||
import TicketProjectsModal from './TicketProjectsModal'
|
||||
import TicketRoadmapPanel from './TicketRoadmapPanel'
|
||||
@@ -17,18 +17,19 @@ import InvoicePanel from './InvoicePanel'
|
||||
import DocumentsPanel from './DocumentsPanel'
|
||||
import Tabs from './ui/Tabs'
|
||||
import Badge from './ui/Badge'
|
||||
import { StatusPill, PriorityPill } from './ui/StatusPill'
|
||||
import { StatusPill, UrgentPill } from './ui/StatusPill'
|
||||
import { useWorksheets } from '../hooks/useWorksheets'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||
import { serviceTypeLabel, canEditWorksheetTimes } from '../lib/ticketForm'
|
||||
import { canEditWorksheetTimes } from '../lib/ticketForm'
|
||||
import { ticketTypeLabel } from '../lib/ticketStatus'
|
||||
|
||||
// Projekt / Preview steht mit im Details-Reiter
|
||||
const TABS = [
|
||||
{ id: 'details', label: 'Details' },
|
||||
{ id: 'worksheets', label: 'Arbeitsblaetter' },
|
||||
{ id: 'worksheets', label: 'Arbeitsblätter' },
|
||||
{ id: 'invoices', label: 'Rechnungen' },
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
{ id: 'project', label: 'Projekt / Preview' },
|
||||
]
|
||||
|
||||
export default function TicketCard({ ticket, onUpdate }) {
|
||||
@@ -89,9 +90,8 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
|
||||
<div className="tcard-badges">
|
||||
<StatusPill status={ticket.status} />
|
||||
<PriorityPill priority={ticket.priority} />
|
||||
{ticket.type && <Badge tone="muted">{ticket.type}</Badge>}
|
||||
{ticket.serviceType && <Badge tone="info">{serviceTypeLabel(ticket.serviceType)}</Badge>}
|
||||
<UrgentPill priority={ticket.priority} />
|
||||
{ticket.type && <Badge tone="muted">{ticketTypeLabel(ticket.type)}</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,9 +99,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
<div style={{ minWidth: 120 }}>
|
||||
<StatusDropdown value={ticket.status} onChange={(v) => onUpdate(ticket.$id, { status: v })} />
|
||||
</div>
|
||||
<div style={{ minWidth: 110 }}>
|
||||
<PriorityDropdown value={ticket.priority} onChange={(v) => onUpdate(ticket.$id, { priority: 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>
|
||||
@@ -121,13 +119,18 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
<div className="ticket-tab-content">
|
||||
{activeTab === 'details' && (
|
||||
<div>
|
||||
<div className="ui-card pad">
|
||||
<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} />
|
||||
<TicketRoadmapPanel ticket={ticket} />
|
||||
<button className="ui-btn ui-btn-primary" style={{ marginBottom: 12 }} onClick={() => setShowProjectsModal(true)}>
|
||||
<FaPen /> Projekte zuweisen / bearbeiten
|
||||
</button>
|
||||
<TicketAssignedProjectsPanel ticket={ticket} refreshKey={projectsRefreshKey} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -135,15 +138,10 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
<div>
|
||||
<div className="flex gap-2" style={{ marginBottom: 16 }}>
|
||||
<button className="ui-btn ui-btn-primary grow" onClick={() => setShowCreateWorksheet(true)}>
|
||||
<FaPlus /> Arbeitsblatt hinzufuegen
|
||||
<FaPlus /> Arbeitsblatt hinzufügen
|
||||
</button>
|
||||
<button className="ui-btn" onClick={() => setShowProjectsModal(true)} title="Projekte"><FaPen /></button>
|
||||
</div>
|
||||
{worksheets.length > 0 && (
|
||||
<div className="ui-card pad" style={{ marginBottom: 16 }}>
|
||||
<WorksheetStats worksheets={worksheets} />
|
||||
</div>
|
||||
)}
|
||||
<WorksheetSummary worksheets={worksheets} />
|
||||
<WorksheetList
|
||||
worksheets={worksheets}
|
||||
totalTime={getTotalTime()}
|
||||
@@ -157,15 +155,6 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
|
||||
{activeTab === 'invoices' && <InvoicePanel ticket={ticket} />}
|
||||
{activeTab === 'documents' && <DocumentsPanel ticket={ticket} />}
|
||||
{activeTab === 'project' && (
|
||||
<div>
|
||||
<TicketRoadmapPanel ticket={ticket} />
|
||||
<button className="ui-btn ui-btn-primary" style={{ marginBottom: 12 }} onClick={() => setShowProjectsModal(true)}>
|
||||
<FaPen /> Projekte zuweisen / bearbeiten
|
||||
</button>
|
||||
<TicketAssignedProjectsPanel ticket={ticket} refreshKey={projectsRefreshKey} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isUrgent, priorityFor } from '../lib/ticketStatus'
|
||||
|
||||
// Ein Klick markiert ein Ticket als dringend oder wieder als normal
|
||||
export default function UrgentToggle({ value, onChange }) {
|
||||
const urgent = isUrgent(value)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`urgent-toggle${urgent ? ' on' : ''}`}
|
||||
aria-pressed={urgent}
|
||||
title={urgent ? 'Nicht mehr dringend' : 'Als dringend markieren'}
|
||||
onClick={() => onChange(priorityFor(!urgent))}
|
||||
>
|
||||
Dringend
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen, FaCheck } from 'react-icons/fa'
|
||||
import { serviceTypeLabel, needsTimeEntry, worksheetTone } from '../lib/ticketForm'
|
||||
import { statusLabel } from '../lib/ticketStatus'
|
||||
|
||||
// Kopffarbe je Zustand (worksheetTone): orange = Git-Zeit fehlt, gruen = Git-Zeit eingetragen
|
||||
const TONES = {
|
||||
@@ -29,7 +30,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
|
||||
return (
|
||||
<div className="text-center p-4">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
<span className="visually-hidden">Lädt …</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -39,7 +40,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
|
||||
return (
|
||||
<div className="alert alert-info" role="alert">
|
||||
<FaComment className="me-2" />
|
||||
Noch keine Worksheets vorhanden. Erstelle das erste Worksheet für dieses Ticket.
|
||||
Noch keine Arbeitsblätter. Leg das erste mit „Arbeitsblatt hinzufügen“ an.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -252,9 +253,9 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
|
||||
}}>
|
||||
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
|
||||
<span className="text-muted">Status:</span>{' '}
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{statusLabel(ws.oldStatus)}</span>
|
||||
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{statusLabel(ws.newStatus)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
|
||||
import { serviceTypeLabel } from '../lib/ticketForm'
|
||||
|
||||
export default function WorksheetStats({ worksheets }) {
|
||||
if (!worksheets || worksheets.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Gesamtarbeitszeit
|
||||
const totalMinutes = worksheets
|
||||
.filter(ws => !ws.isComment)
|
||||
.reduce((sum, ws) => sum + (ws.totalTime || 0), 0)
|
||||
|
||||
// Nach Mitarbeiter gruppieren
|
||||
const byEmployee = worksheets.reduce((acc, ws) => {
|
||||
const empId = ws.employeeId
|
||||
if (!acc[empId]) {
|
||||
acc[empId] = {
|
||||
name: ws.employeeName,
|
||||
short: ws.employeeShort,
|
||||
time: 0,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
if (!ws.isComment) {
|
||||
acc[empId].time += ws.totalTime || 0
|
||||
}
|
||||
acc[empId].count += 1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
||||
// Service Type Verteilung (Remote und das alte Off Site zusammen als Fernwartung)
|
||||
const byServiceType = worksheets.reduce((acc, ws) => {
|
||||
const type = serviceTypeLabel(ws.serviceType) || 'Unknown'
|
||||
acc[type] = (acc[type] || 0) + 1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const formatTime = (minutes) => {
|
||||
if (!minutes || minutes === 0) return '0min'
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="worksheet-stats" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
height: '100%'
|
||||
}}>
|
||||
{/* Gesamtübersicht */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '0 0 auto'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<FaChartLine size={16} style={{ color: '#10b981' }} />
|
||||
Gesamtübersicht
|
||||
</h6>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Worksheets:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.length}</strong>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Arbeitszeit:</span>
|
||||
<strong style={{ color: '#10b981', fontSize: '16px' }}>{formatTime(totalMinutes)}</strong>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Kommentare:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.filter(ws => ws.isComment).length}</strong>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Ø pro WS:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>
|
||||
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mitarbeiter Kombiniertes Diagramm */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '1 1 auto',
|
||||
minHeight: '200px'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<FaUsers size={16} style={{ color: '#10b981' }} />
|
||||
Mitarbeiter-Statistiken
|
||||
</h6>
|
||||
|
||||
{Object.keys(byEmployee).length === 0 ? (
|
||||
<div style={{ color: '#a0aec0', textAlign: 'center', padding: '20px' }}>
|
||||
Keine Mitarbeiter-Daten verfügbar
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{(() => {
|
||||
const employeeArray = Object.values(byEmployee).sort((a, b) => b.time - a.time)
|
||||
const maxTime = Math.max(...employeeArray.map(e => e.time), 1)
|
||||
|
||||
return employeeArray.map((emp, idx) => {
|
||||
const percentage = maxTime > 0 ? (emp.time / maxTime) * 100 : 0
|
||||
return (
|
||||
<div key={idx}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '6px',
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontWeight: '500',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
minWidth: '120px'
|
||||
}}>
|
||||
{emp.short && (
|
||||
<span style={{
|
||||
background: 'rgba(16, 185, 129, 0.2)',
|
||||
color: '#10b981',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '9px',
|
||||
fontWeight: 'bold'
|
||||
}}>{emp.short}</span>
|
||||
)}
|
||||
<span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{emp.name}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '28px',
|
||||
background: 'rgba(26, 32, 44, 0.6)',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{/* WS Anzahl am Anfang */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '8px',
|
||||
zIndex: 2,
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span>WS</span>
|
||||
<span style={{
|
||||
background: 'rgba(255, 255, 255, 0.3)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
}}>{emp.count}</span>
|
||||
</div>
|
||||
|
||||
{/* Balken mit Zeit */}
|
||||
<div style={{
|
||||
width: `${percentage}%`,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #10b981 0%, #059669 100%)',
|
||||
borderRadius: '6px',
|
||||
transition: 'width 0.5s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: '8px',
|
||||
paddingLeft: '60px',
|
||||
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Zeit am Ende des Balkens */}
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{formatTime(emp.time)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Zeit außerhalb des Balkens (falls Balken zu kurz) */}
|
||||
{percentage < 30 && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
zIndex: 2,
|
||||
color: '#10b981',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{formatTime(emp.time)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service Type Verteilung */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '0 0 auto'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<FaClock size={16} style={{ color: '#10b981' }} />
|
||||
Service Types
|
||||
</h6>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{Object.entries(byServiceType).map(([type, count]) => (
|
||||
<div key={type} style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
}}>{type}</span>
|
||||
<strong style={{
|
||||
color: '#10b981',
|
||||
fontSize: '14px'
|
||||
}}>{count}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { needsTimeEntry } from '../lib/ticketForm'
|
||||
|
||||
const formatTime = (minutes) => {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
|
||||
}
|
||||
|
||||
// Eine Zeile ueber der Arbeitsblatt-Liste: Arbeitszeit, Anzahl, offene Git-Zeiten
|
||||
export default function WorksheetSummary({ worksheets }) {
|
||||
if (!worksheets || worksheets.length === 0) return null
|
||||
const minutes = worksheets
|
||||
.filter((ws) => !ws.isComment)
|
||||
.reduce((sum, ws) => sum + (Number(ws.totalTime) || 0), 0)
|
||||
const open = worksheets.filter(needsTimeEntry).length
|
||||
|
||||
return (
|
||||
<div className="worksheet-summary">
|
||||
<span>Arbeitszeit gesamt <strong>{formatTime(minutes)}</strong></span>
|
||||
<span>{worksheets.length} {worksheets.length === 1 ? 'Arbeitsblatt' : 'Arbeitsblätter'}</span>
|
||||
{open > 0 && <span className="open">{open} {open === 1 ? 'Zeit offen' : 'Zeiten offen'}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,4 @@
|
||||
const STATUS_CLASS = {
|
||||
Open: 'status-open',
|
||||
Occupied: 'status-occupied',
|
||||
Assigned: 'status-assigned',
|
||||
Awaiting: 'status-awaiting',
|
||||
'Added Info': 'status-awaiting',
|
||||
Closed: 'status-closed',
|
||||
}
|
||||
|
||||
const PRIORITY = {
|
||||
0: { cls: 'priority-none', label: 'Keine' },
|
||||
1: { cls: 'priority-low', label: 'Niedrig' },
|
||||
2: { cls: 'priority-medium', label: 'Mittel' },
|
||||
3: { cls: 'priority-high', label: 'Hoch' },
|
||||
4: { cls: 'priority-critical', label: 'Kritisch' },
|
||||
}
|
||||
import { statusLabel, statusClass, isUrgent } from '../../lib/ticketStatus'
|
||||
|
||||
const pillBase = {
|
||||
display: 'inline-flex',
|
||||
@@ -26,11 +11,11 @@ const pillBase = {
|
||||
}
|
||||
|
||||
export function StatusPill({ status }) {
|
||||
const cls = STATUS_CLASS[status] || 'status-open'
|
||||
return <span className={cls} style={pillBase}>{status || 'Open'}</span>
|
||||
return <span className={statusClass(status)} style={pillBase}>{statusLabel(status)}</span>
|
||||
}
|
||||
|
||||
export function PriorityPill({ priority }) {
|
||||
const p = PRIORITY[priority] ?? PRIORITY[1]
|
||||
return <span className={p.cls} style={pillBase}>{p.label}</span>
|
||||
// Nur dringende Tickets bekommen ein Abzeichen
|
||||
export function UrgentPill({ priority }) {
|
||||
if (!isUrgent(priority)) return null
|
||||
return <span className="priority-critical" style={pillBase}>Dringend</span>
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export { default as PageHeader } from './PageHeader'
|
||||
export { default as Card } from './Card'
|
||||
export { default as Button } from './Button'
|
||||
export { default as Badge } from './Badge'
|
||||
export { StatusPill, PriorityPill } from './StatusPill'
|
||||
export { StatusPill, UrgentPill } from './StatusPill'
|
||||
export { default as Tabs } from './Tabs'
|
||||
export { default as EmptyState } from './EmptyState'
|
||||
export { default as Field } from './Field'
|
||||
|
||||
Reference in new issue
Block a user