Neues Ticket: Formular entschlackt, Kalender und Autovervollständigung

- Work Order Type nur noch Webpage und neu Migration (Kunde kommt von anderem Anbieter)
- Affected System, Response Level und Benachrichtigungsfelder entfernt
- Service Type: Fernwartung (remote) oder Vor Ort beim Kunden, Standard Fernwartung;
  gespeichert werden weiter "Remote" / "On Site"
- Start und Ende mit Kalender und Uhrzeitfeld; gespeichert weiter als dd.mm.yyyy / hhmm
- Requested by/for schlagen Kunden, Mitarbeiter und Namen aus den letzten Tickets vor,
  die des gewählten Kunden zuerst
- Neue Tickets speichern Kundenname und Ort (die Karte zeigte sonst "Unbekannt")
- Ticketfilter kennt Webpage und Migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 19:30:29 +00:00
1 parent cf1d653eb2
commit d86f9c07cd
7 files changed
+356 -163

No files matched your search

+124 -163
View File
@@ -1,30 +1,30 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useMemo } from 'react'
import { FaTimes } from 'react-icons/fa' import { FaTimes } from 'react-icons/fa'
import { useAdminConfig } from '../hooks/useAdminConfig' 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 {
todayIso,
isoToGermanDate,
timeToWoms,
buildRequesterSuggestions,
rankSuggestions,
} from '../lib/ticketForm'
import { AutocompleteInput } from './ui'
// Fallback-Werte falls Config nicht geladen werden kann // Neue Tickets gibt es aktuell nur fuer diese Arten; alte Tickets behalten ihre Art.
const DEFAULT_TICKET_TYPES = [ // Migration = ein Kunde, den wir von einem anderen Anbieter uebernehmen.
'Akquise', 'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request', const TICKET_TYPES = [
'Maintenance', 'Project', 'Webpage', 'Controlling', 'Development', 'Documentation', { value: 'Webpage', label: 'Webpage' },
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement', { value: 'Migration', label: 'Migration (Übernahme von anderem Anbieter)' },
'Rollout', 'Emergency Call', 'Other Services'
] ]
const DEFAULT_SYSTEMS = [ // Gespeichert werden die bisherigen Werte, angezeigt wird Deutsch
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware', const SERVICE_TYPES = [
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network', { value: 'Remote', label: 'Fernwartung (remote)' },
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking', { value: 'On Site', label: 'Vor Ort beim Kunden' },
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
] ]
const DEFAULT_RESPONSE_LEVELS = [
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
]
const DEFAULT_SERVICE_TYPES = ['Remote', 'On Site', 'Off Site']
const DEFAULT_PRIORITIES = [ const DEFAULT_PRIORITIES = [
{ value: 0, label: 'None' }, { value: 0, label: 'None' },
{ value: 1, label: 'Low' }, { value: 1, label: 'Low' },
@@ -33,56 +33,68 @@ const DEFAULT_PRIORITIES = [
{ value: 4, label: 'Critical' } { value: 4, label: 'Critical' }
] ]
const today = new Date().toLocaleDateString('de-DE') // Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder)
const emptyForm = () => ({
customerId: '',
type: TICKET_TYPES[0].value,
serviceType: SERVICE_TYPES[0].value,
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: todayIso(),
startTime: '',
deadline: todayIso(),
endTime: '',
estimate: '30',
details: ''
})
// Klick irgendwo ins Datumsfeld oeffnet den Kalender, nicht nur das kleine Symbol
const openCalendar = (e) => {
try {
e.currentTarget.showPicker?.()
} catch {
// aeltere Browser: Kalender bleibt ueber das Symbol erreichbar
}
}
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) { export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
const { config } = useAdminConfig() const { config } = useAdminConfig()
const { employees } = useEmployees() const { employees } = useEmployees()
// Verwende Config-Werte oder Fallbacks
const TICKET_TYPES = config?.ticketTypes || DEFAULT_TICKET_TYPES
const SYSTEMS = config?.systems || DEFAULT_SYSTEMS
const RESPONSE_LEVELS = config?.responseLevels || DEFAULT_RESPONSE_LEVELS
const SERVICE_TYPES = config?.serviceTypes || DEFAULT_SERVICE_TYPES
const PRIORITIES = config?.priorities || DEFAULT_PRIORITIES const PRIORITIES = config?.priorities || DEFAULT_PRIORITIES
const [formData, setFormData] = useState({ const [formData, setFormData] = useState(emptyForm)
customerId: '', const [recentTickets, setRecentTickets] = useState([])
type: '',
systemType: '',
responseLevel: '',
serviceType: '',
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
// Setze Default-Werte wenn Config geladen ist oder Modal geöffnet wird // Reset error when modal opens
useEffect(() => { useEffect(() => {
if (isOpen && (TICKET_TYPES.length > 0 || SERVICE_TYPES.length > 0)) { if (isOpen) setError('')
setFormData(prev => ({ }, [isOpen])
...prev,
type: prev.type || TICKET_TYPES[0] || 'Supportrequest', // Namen aus den letzten Tickets fuer die Autovervollstaendigung
serviceType: prev.serviceType || SERVICE_TYPES[0] || 'Remote', useEffect(() => {
priority: prev.priority || (PRIORITIES.find(p => p.value === 1)?.value || 1) if (!isOpen || isDemoMode) return
})) let cancelled = false
databases
.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.orderDesc('$createdAt'), Query.limit(200)])
.then((response) => {
if (!cancelled) setRecentTickets(response.documents || [])
})
.catch((err) => console.warn('Vorschläge aus früheren Tickets nicht geladen:', err))
return () => {
cancelled = true
} }
// Reset error when modal opens }, [isOpen])
if (isOpen) {
setError('') const suggestions = useMemo(
} () => buildRequesterSuggestions({ customers, employees, tickets: recentTickets }),
}, [isOpen, TICKET_TYPES, SERVICE_TYPES, PRIORITIES]) [customers, employees, recentTickets]
)
const getSuggestions = (text) => rankSuggestions(suggestions, text, { customerId: formData.customerId })
const handleChange = (field, value) => { const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value })) setFormData(prev => ({ ...prev, [field]: value }))
@@ -90,34 +102,35 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
if (error) setError('') if (error) setError('')
} }
// Das Ende darf nicht vor dem Start liegen, deshalb rueckt es mit
const handleStartDateChange = (value) => {
setFormData(prev => ({
...prev,
startDate: value,
deadline: value && prev.deadline && prev.deadline < value ? value : prev.deadline
}))
if (error) setError('')
}
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
setLoading(true) setLoading(true)
setError('') setError('')
const customer = customers.find(c => c.$id === formData.customerId)
try { try {
const result = await onCreate(formData) const result = await onCreate({
...formData,
customerName: customer?.name || '',
customerLocation: customer?.location || '',
startDate: isoToGermanDate(formData.startDate),
startTime: timeToWoms(formData.startTime),
deadline: isoToGermanDate(formData.deadline),
endTime: timeToWoms(formData.endTime)
})
if (result.success) { if (result.success) {
onClose() onClose()
setFormData({ setFormData(emptyForm())
customerId: '',
type: TICKET_TYPES[0] || 'Supportrequest',
systemType: '',
responseLevel: '',
serviceType: SERVICE_TYPES[0] || 'Remote',
priority: PRIORITIES.find(p => p.value === 1)?.value || 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
} else { } else {
setError(result.error || 'Fehler beim Erstellen des Tickets') setError(result.error || 'Fehler beim Erstellen des Tickets')
} }
@@ -138,13 +151,13 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
</span> </span>
<div className="overlay-content"> <div className="overlay-content">
<h2 className="mb-2">Create New Ticket</h2> <h2 className="mb-2">Create New Ticket</h2>
{error && ( {error && (
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}> <div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="row"> <div className="row">
<div className="col col-6"> <div className="col col-6">
@@ -171,36 +184,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
onChange={(e) => handleChange('type', e.target.value)} onChange={(e) => handleChange('type', e.target.value)}
> >
{TICKET_TYPES.map(type => ( {TICKET_TYPES.map(type => (
<option key={type} value={type}>{type}</option> <option key={type.value} value={type.value}>{type.label}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Affected System</label>
<select
className="form-control"
value={formData.systemType}
onChange={(e) => handleChange('systemType', e.target.value)}
required
>
<option value="">Affected System</option>
{SYSTEMS.map(sys => (
<option key={sys} value={sys}>{sys}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Response Level</label>
<select
className="form-control"
value={formData.responseLevel}
onChange={(e) => handleChange('responseLevel', e.target.value)}
>
<option value="">Response Level</option>
{RESPONSE_LEVELS.map(level => (
<option key={level} value={level}>{level}</option>
))} ))}
</select> </select>
</div> </div>
@@ -213,7 +197,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
onChange={(e) => handleChange('serviceType', e.target.value)} onChange={(e) => handleChange('serviceType', e.target.value)}
> >
{SERVICE_TYPES.map(type => ( {SERVICE_TYPES.map(type => (
<option key={type} value={type}>{type}</option> <option key={type.value} value={type.value}>{type.label}</option>
))} ))}
</select> </select>
</div> </div>
@@ -257,68 +241,67 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Requested by</label> <label className="form-label" htmlFor="ticket-requested-by">Requested by</label>
<input <AutocompleteInput
type="text" id="ticket-requested-by"
className="form-control"
placeholder="Name" placeholder="Name"
value={formData.requestedBy} value={formData.requestedBy}
onChange={(e) => handleChange('requestedBy', e.target.value)} onChange={(value) => handleChange('requestedBy', value)}
getSuggestions={getSuggestions}
required required
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Requested for</label> <label className="form-label" htmlFor="ticket-requested-for">Requested for</label>
<input <AutocompleteInput
type="text" id="ticket-requested-for"
className="form-control"
placeholder="Name" placeholder="Name"
value={formData.requestedFor} value={formData.requestedFor}
onChange={(e) => handleChange('requestedFor', e.target.value)} onChange={(value) => handleChange('requestedFor', value)}
getSuggestions={getSuggestions}
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Start Date & Time</label> <label className="form-label" htmlFor="ticket-start-date">Start Date & Time</label>
<div style={{ display: 'flex', gap: '8px' }}> <div className="datetime-pair">
<input <input
type="text" id="ticket-start-date"
type="date"
className="form-control" className="form-control"
placeholder="dd.mm.yyyy"
value={formData.startDate} value={formData.startDate}
onChange={(e) => handleChange('startDate', e.target.value)} onChange={(e) => handleStartDateChange(e.target.value)}
style={{ flex: 1 }} onClick={openCalendar}
/> />
<input <input
type="text" type="time"
className="form-control" className="form-control"
placeholder="hhmm" aria-label="Start time"
value={formData.startTime} value={formData.startTime}
onChange={(e) => handleChange('startTime', e.target.value)} onChange={(e) => handleChange('startTime', e.target.value)}
style={{ flex: 1 }}
/> />
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">End Date & Time (Deadline)</label> <label className="form-label" htmlFor="ticket-end-date">End Date & Time (Deadline)</label>
<div style={{ display: 'flex', gap: '8px' }}> <div className="datetime-pair">
<input <input
type="text" id="ticket-end-date"
type="date"
className="form-control" className="form-control"
placeholder="dd.mm.yyyy" min={formData.startDate || undefined}
value={formData.deadline} value={formData.deadline}
onChange={(e) => handleChange('deadline', e.target.value)} onChange={(e) => handleChange('deadline', e.target.value)}
style={{ flex: 1 }} onClick={openCalendar}
/> />
<input <input
type="text" type="time"
className="form-control" className="form-control"
placeholder="hhmm" aria-label="End time"
value={formData.endTime} value={formData.endTime}
onChange={(e) => handleChange('endTime', e.target.value)} onChange={(e) => handleChange('endTime', e.target.value)}
style={{ flex: 1 }}
/> />
</div> </div>
</div> </div>
@@ -333,28 +316,6 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
onChange={(e) => handleChange('estimate', e.target.value)} onChange={(e) => handleChange('estimate', e.target.value)}
/> />
</div> </div>
<div className="form-group">
<label className="form-label">Notification Recipient(s)</label>
<input
type="text"
className="form-control"
placeholder="name@domain.xx"
value={formData.mailCopyTo}
onChange={(e) => handleChange('mailCopyTo', e.target.value)}
/>
</div>
<div className="form-group">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={formData.sendNotification}
onChange={(e) => handleChange('sendNotification', e.target.checked)}
/>
Send Notification?
</label>
</div>
</div> </div>
</div> </div>
@@ -370,8 +331,8 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
</div> </div>
<div className="text-center mt-2"> <div className="text-center mt-2">
<button <button
type="submit" type="submit"
className="btn btn-dark" className="btn btn-dark"
disabled={loading} disabled={loading}
> >
+87
View File
@@ -0,0 +1,87 @@
import { useId, useState } from 'react'
/* Textfeld mit Vorschlagsliste. Freie Eingaben bleiben erlaubt;
getSuggestions(text) liefert [{ value, hint }]. */
export default function AutocompleteInput({ value, onChange, getSuggestions, id, placeholder, required }) {
const listId = useId()
const [open, setOpen] = useState(false)
const [active, setActive] = useState(-1)
const items = open ? getSuggestions(value) : []
const showList = items.length > 0
const choose = (item) => {
onChange(item.value)
setOpen(false)
setActive(-1)
}
const handleKeyDown = (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
setOpen(true)
setActive((i) => Math.min(i + 1, items.length - 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setActive((i) => Math.max(i - 1, -1))
} else if (e.key === 'Enter' && showList && items[active]) {
e.preventDefault()
choose(items[active])
} else if (e.key === 'Escape' && showList) {
e.preventDefault()
setOpen(false)
}
}
return (
<div className="autocomplete">
<input
type="text"
id={id}
className="form-control"
role="combobox"
aria-autocomplete="list"
aria-expanded={showList}
aria-controls={listId}
aria-activedescendant={showList && items[active] ? `${listId}-${active}` : undefined}
autoComplete="off"
placeholder={placeholder}
required={required}
value={value}
onChange={(e) => {
onChange(e.target.value)
setOpen(true)
setActive(-1)
}}
onFocus={() => setOpen(true)}
onBlur={() => {
setOpen(false)
setActive(-1)
}}
onKeyDown={handleKeyDown}
/>
{showList && (
<ul className="autocomplete-list" id={listId} role="listbox">
{items.map((item, index) => (
<li
key={item.value}
id={`${listId}-${index}`}
role="option"
aria-selected={index === active}
className={`autocomplete-item${index === active ? ' active' : ''}`}
// mousedown statt click: sonst verliert das Feld vorher den Fokus und die Liste schliesst
onMouseDown={(e) => {
e.preventDefault()
choose(item)
}}
onMouseEnter={() => setActive(index)}
>
<span className="autocomplete-value">{item.value}</span>
{item.hint && <span className="autocomplete-hint">{item.hint}</span>}
</li>
))}
</ul>
)}
</div>
)
}
+1
View File
@@ -7,3 +7,4 @@ export { default as Tabs } from './Tabs'
export { default as EmptyState } from './EmptyState' export { default as EmptyState } from './EmptyState'
export { default as Field } from './Field' export { default as Field } from './Field'
export { default as Kpi } from './Kpi' export { default as Kpi } from './Kpi'
export { default as AutocompleteInput } from './AutocompleteInput'
+2
View File
@@ -271,6 +271,8 @@ export function useWorkorders(filters = {}) {
responseLevel: data.responseLevel || '', responseLevel: data.responseLevel || '',
serviceType: data.serviceType || 'Remote', serviceType: data.serviceType || 'Remote',
customerId: data.customerId || '', customerId: data.customerId || '',
customerName: data.customerName || '',
customerLocation: data.customerLocation || '',
assignedTo: automatic.assignedTo, assignedTo: automatic.assignedTo,
assignedName: automatic.assignedName, assignedName: automatic.assignedName,
requestedBy: data.requestedBy || '', requestedBy: data.requestedBy || '',
+94
View File
@@ -0,0 +1,94 @@
/* Hilfen fuer "Neues Ticket".
WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm"
(siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen
"yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */
const pad = (n) => String(n).padStart(2, '0')
// Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">)
export function todayIso(now = new Date()) {
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
}
// "2026-09-26" -> "26.09.2026"
export function isoToGermanDate(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ''))
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
}
// "09:30" -> "0930"
export function timeToWoms(value) {
const m = /^(\d{1,2}):(\d{2})/.exec(String(value || ''))
return m ? `${m[1].padStart(2, '0')}${m[2]}` : ''
}
/* Vorschlaege fuer "Requested by" / "Requested for": Kunden, Mitarbeiter
und Namen aus frueheren Tickets. Pro Name (ohne Gross-/Kleinschreibung)
ein Eintrag; customerIds merkt sich, zu welchen Kunden er gehoert. */
export function buildRequesterSuggestions({ customers = [], employees = [], tickets = [] }) {
const byKey = new Map()
const add = (value, { hint = '', search = '', customerId = '', used = false } = {}) => {
const text = String(value || '').trim()
if (!text) return
const key = text.toLowerCase()
let entry = byKey.get(key)
if (!entry) {
entry = { value: text, hint: '', search: key, customerIds: new Set(), uses: 0 }
byKey.set(key, entry)
}
if (hint && !entry.hint) entry.hint = hint
if (search) entry.search += ` ${search.toLowerCase()}`
if (customerId) entry.customerIds.add(customerId)
if (used) entry.uses += 1
}
for (const customer of customers) {
add(customer.name || customer.companyName, {
hint: customer.email ? `Kunde · ${customer.email}` : 'Kunde',
search: customer.email || '',
customerId: customer.$id,
})
}
for (const employee of employees) {
add(employee.displayName, { hint: 'Mitarbeiter', search: employee.email || '' })
}
for (const ticket of tickets) {
add(ticket.requestedBy, { customerId: ticket.customerId, used: true })
add(ticket.requestedFor, { customerId: ticket.customerId, used: true })
}
for (const entry of byKey.values()) {
if (!entry.hint) entry.hint = 'aus früheren Tickets'
}
return [...byKey.values()]
}
/* Passende Vorschlaege zur Eingabe, beste zuerst. Namen des gewaehlten
Kunden stehen oben; ohne Eingabe gibt es nur diese und schon benutzte
Namen, damit die Liste nicht einfach alle Kunden aufzaehlt. */
export function rankSuggestions(entries, query, { customerId = '', limit = 8 } = {}) {
const q = String(query || '').trim().toLowerCase()
const ranked = []
for (const entry of entries) {
const value = entry.value.toLowerCase()
if (q && value === q) continue
const forCustomer = Boolean(customerId) && entry.customerIds.has(customerId)
let score
if (!q) {
if (!forCustomer && !entry.uses) continue
score = 0
} else if (value.startsWith(q)) {
score = 3
} else if (value.split(/[\s.@-]+/).some((word) => word.startsWith(q))) {
score = 2
} else if (entry.search.includes(q)) {
score = 1
} else {
continue
}
if (forCustomer) score += 2
score += Math.min(entry.uses, 10) / 10
ranked.push({ entry, score })
}
ranked.sort((a, b) => b.score - a.score || a.entry.value.localeCompare(b.entry.value, 'de'))
return ranked.slice(0, limit).map(({ entry }) => entry)
}
+1
View File
@@ -104,6 +104,7 @@ export default function TicketsPage() {
<input type="date" className="form-control" value={localFilters.createdDate} onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })} /> <input type="date" className="form-control" value={localFilters.createdDate} onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })} />
<select className="form-control" value={localFilters.type} onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}> <select className="form-control" value={localFilters.type} onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}>
<option value="">Typ / Ort</option> <option value="">Typ / Ort</option>
<option>Webpage</option><option>Migration</option>
<option>Supportrequest</option><option>Change Request</option><option>Maintenance</option> <option>Supportrequest</option><option>Change Request</option><option>Maintenance</option>
<option>Project</option><option>Procurement</option><option>Emergency Call</option> <option>Project</option><option>Procurement</option><option>Emergency Call</option>
<option>Home Office</option><option>Holidays</option><option>Trip</option> <option>Home Office</option><option>Holidays</option><option>Trip</option>
+47
View File
@@ -1129,3 +1129,50 @@ textarea.form-control {
font-size: 13px; font-size: 13px;
line-height: 1.45; line-height: 1.45;
} }
/* Neues Ticket: Datum + Uhrzeit nebeneinander; dunkle Kalender statt weisser */
.datetime-pair { display: flex; gap: 8px; }
.datetime-pair .form-control { flex: 1; min-width: 0; color-scheme: dark; }
.datetime-pair input[type="date"] { cursor: pointer; }
/* Autovervollstaendigung (Requested by / Requested for) */
.autocomplete { position: relative; }
.autocomplete-list {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 20;
margin: 0;
padding: 4px;
list-style: none;
max-height: 280px;
overflow-y: auto;
background: var(--surface-3);
border: 1px solid var(--border-strong);
border-radius: var(--r-sm);
box-shadow: var(--shadow-3);
}
.autocomplete-item {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-radius: 6px;
color: var(--text);
font-size: 14px;
cursor: pointer;
}
.autocomplete-item.active { background: var(--accent-soft); }
.autocomplete-item.active .autocomplete-value { color: var(--accent); }
.autocomplete-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.autocomplete-hint {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-faint);
font-size: 12px;
}