Tickets: Abschlussfixes P2 - Termin ohne Umbruch in der Zeitspanne, Art in der Liste, Laden statt Leerzustand, Unterstuetzung entsperrt bei Fehler, Aufraeumen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
504571c0ce
commit
ed8bdba015
8 files changed
+38
-23
No files matched your search
@@ -23,13 +23,18 @@ export default function OwnSupportModal({ worksheet, currentUserId, onClose, onS
|
|||||||
const save = async (value) => {
|
const save = async (value) => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError('')
|
setError('')
|
||||||
const result = await onSave(worksheet, value)
|
try {
|
||||||
if (result?.success) {
|
const result = await onSave(worksheet, value)
|
||||||
onClose()
|
if (result?.success) {
|
||||||
return
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(result?.error || 'Speichern fehlgeschlagen')
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Speichern fehlgeschlagen')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
}
|
}
|
||||||
setError(result?.error || 'Speichern fehlgeschlagen')
|
|
||||||
setSaving(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export default function WorksheetTimesModal({ worksheet, currentUserId, onClose,
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
as="form"
|
as="form"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const TICKET_TABS = ['details', 'arbeitsblaetter']
|
|||||||
|
|
||||||
// Das Ticket wird ueber seine woid adressiert, sonst ueber die $id (nie ueber die Kurznummer)
|
// Das Ticket wird ueber seine woid adressiert, sonst ueber die $id (nie ueber die Kurznummer)
|
||||||
export function ticketRef(ticket) {
|
export function ticketRef(ticket) {
|
||||||
return String(ticket?.woid || ticket?.$id || '')
|
return String(ticket?.woid ?? '').trim() || String(ticket?.$id ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ticketPath(ref, tab) {
|
export function ticketPath(ref, tab) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Folgen eines neuen Arbeitsblatts (bisher TicketCard.handleCreateWorksheet).
|
/* Folgen eines neuen Arbeitsblatts.
|
||||||
Kennt kein Appwrite: die Seite reicht die Schreibzugriffe herein. */
|
Kennt kein Appwrite: die Seite reicht die Schreibzugriffe herein. */
|
||||||
|
|
||||||
// Akquise-Automatik: das Ergebnis im Arbeitsblatt steuert den Kundenstatus.
|
// Akquise-Automatik: das Ergebnis im Arbeitsblatt steuert den Kundenstatus.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function TicketPage() {
|
|||||||
if (error || !ticket) {
|
if (error || !ticket) {
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<p className="msg err" role="alert">Fehler: {error || 'Das Ticket konnte nicht geladen werden.'}</p>
|
<p className="msg err" role="alert">Fehler: {error}</p>
|
||||||
<div className="mt-2">{back}</div>
|
<div className="mt-2">{back}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -63,6 +63,18 @@ export default function TicketPage() {
|
|||||||
return <TicketView key={ticket.$id} ticket={ticket} updateWorkorder={updateWorkorder} tab={activeTab} />
|
return <TicketView key={ticket.$id} ticket={ticket} updateWorkorder={updateWorkorder} tab={activeTab} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datum und Uhrzeitspanne bleiben je in einer Zeile: umbrechen darf nur das Komma dazwischen.
|
||||||
|
// Rohtext ohne ", " (unlesbare Altwerte) bricht weiter frei um.
|
||||||
|
function AppointmentText({ text }) {
|
||||||
|
const at = text.indexOf(', ')
|
||||||
|
if (at === -1) return text
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="nowrap">{text.slice(0, at)}</span>, <span className="nowrap">{text.slice(at + 2)}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function TicketView({ ticket, updateWorkorder, tab }) {
|
function TicketView({ ticket, updateWorkorder, tab }) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -204,7 +216,7 @@ function TicketView({ ticket, updateWorkorder, tab }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="fact">
|
<div className="fact">
|
||||||
<div className="k">Termin</div>
|
<div className="k">Termin</div>
|
||||||
<div className="v">{appointment || <span className="faint">Kein Termin</span>}</div>
|
<div className="v">{appointment ? <AppointmentText text={appointment} /> : <span className="faint">Kein Termin</span>}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="fact">
|
<div className="fact">
|
||||||
<div className="k">Fällig</div>
|
<div className="k">Fällig</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from '../lib/ticketListUrl'
|
} from '../lib/ticketListUrl'
|
||||||
import { isRowActivation } from '../lib/rowClick'
|
import { isRowActivation } from '../lib/rowClick'
|
||||||
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
|
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
|
||||||
import { TYPE_FILTERS } from '../lib/ticketStatus'
|
import { TYPE_FILTERS, ticketTypeLabel } from '../lib/ticketStatus'
|
||||||
import { TIME_MAX_ROWS, minutesCellText } from '../lib/ticketTimes'
|
import { TIME_MAX_ROWS, minutesCellText } from '../lib/ticketTimes'
|
||||||
import { appointmentText, assigneeLabel, ticketNumber } from '../lib/ticketView'
|
import { appointmentText, assigneeLabel, ticketNumber } from '../lib/ticketView'
|
||||||
|
|
||||||
@@ -91,12 +91,6 @@ function TicketList() {
|
|||||||
const { employees } = useEmployees()
|
const { employees } = useEmployees()
|
||||||
const { byWoid, capped, version } = useTicketMinutes(workorders)
|
const { byWoid, capped, version } = useTicketMinutes(workorders)
|
||||||
|
|
||||||
// Der Ladehinweis ohne Zeilen gilt nur dem allerersten Laden; danach bleiben die alten Zeilen stehen
|
|
||||||
const [loaded, setLoaded] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loading) setLoaded(true)
|
|
||||||
}, [loading])
|
|
||||||
|
|
||||||
// Mit welchem Limit wurden die gezeigten Zeilen geladen? Nach "Mehr laden" ist state.limit schon größer,
|
// Mit welchem Limit wurden die gezeigten Zeilen geladen? Nach "Mehr laden" ist state.limit schon größer,
|
||||||
// die Zeilen sind es erst mit der Antwort: der Knopf bleibt bis dahin stehen (und ist gesperrt).
|
// die Zeilen sind es erst mit der Antwort: der Knopf bleibt bis dahin stehen (und ist gesperrt).
|
||||||
// Im Ruhezustand heißt das: Zeilen >= state.limit und state.limit unter der Obergrenze von nextLimit.
|
// Im Ruhezustand heißt das: Zeilen >= state.limit und state.limit unter der Obergrenze von nextLimit.
|
||||||
@@ -122,6 +116,9 @@ function TicketList() {
|
|||||||
return { id: tab.id, label: tab.label, count }
|
return { id: tab.id, label: tab.label, count }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Die Art steht in der Zeile, solange der Filter keine einzelne Art festlegt
|
||||||
|
const showType = state.typeId === 'orders' || state.typeId === 'all'
|
||||||
|
|
||||||
// byWoid ist eine an Ort und Stelle ergänzte Map: erst `version` zeigt eine Änderung an
|
// byWoid ist eine an Ort und Stelle ergänzte Map: erst `version` zeigt eine Änderung an
|
||||||
const rows = useMemo(
|
const rows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -147,6 +144,7 @@ function TicketList() {
|
|||||||
<div className="cell-sub">
|
<div className="cell-sub">
|
||||||
{ticket.customerName || 'Unbekannt'}
|
{ticket.customerName || 'Unbekannt'}
|
||||||
{ticket.customerLocation ? ` · ${ticket.customerLocation}` : ''}
|
{ticket.customerLocation ? ` · ${ticket.customerLocation}` : ''}
|
||||||
|
{showType && ticket.type ? ` · ${ticketTypeLabel(ticket.type)}` : ''}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><StatusPill status={ticket.status} /></td>
|
<td><StatusPill status={ticket.status} /></td>
|
||||||
@@ -157,11 +155,12 @@ function TicketList() {
|
|||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
[workorders, employees, byWoid, version, listSearch, navigate]
|
[workorders, employees, byWoid, version, listSearch, navigate, showType]
|
||||||
)
|
)
|
||||||
|
|
||||||
let content
|
let content
|
||||||
if (!loaded && loading) {
|
// Ladezustand, solange keine Zeilen da sind (erstes Laden und Filterwechsel auf eine leere Liste); mit Zeilen bleiben die alten stehen
|
||||||
|
if (loading && workorders.length === 0) {
|
||||||
content = <div className="loading">Wird geladen …</div>
|
content = <div className="loading">Wird geladen …</div>
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
content = <p className="msg err" role="alert">Fehler: {error}</p>
|
content = <p className="msg err" role="alert">Fehler: {error}</p>
|
||||||
|
|||||||
@@ -967,7 +967,6 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
|
|||||||
/* oberhalb durch die Reihenfolge, nie durch eine Erzwingung. */
|
/* oberhalb durch die Reihenfolge, nie durch eine Erzwingung. */
|
||||||
|
|
||||||
/* ---------- Arbeitsblaetter ---------- */
|
/* ---------- Arbeitsblaetter ---------- */
|
||||||
.ws-list { margin-bottom: 8px; }
|
|
||||||
.ws { padding: 16px 0; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: 148px minmax(0, 1fr) auto; gap: 4px 24px; }
|
.ws { padding: 16px 0; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: 148px minmax(0, 1fr) auto; gap: 4px 24px; }
|
||||||
.ws:first-of-type { border-top: 1px solid var(--line); }
|
.ws:first-of-type { border-top: 1px solid var(--line); }
|
||||||
.ws-when { color: var(--text-2); font-size: 13px; line-height: 20px; }
|
.ws-when { color: var(--text-2); font-size: 13px; line-height: 20px; }
|
||||||
@@ -976,11 +975,9 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
|
|||||||
.ws-head .who { font-weight: 600; overflow-wrap: anywhere; }
|
.ws-head .who { font-weight: 600; overflow-wrap: anywhere; }
|
||||||
.ws-text { color: var(--text); margin-top: 4px; max-width: 640px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
.ws-text { color: var(--text); margin-top: 4px; max-width: 640px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
.ws-support { margin-top: 8px; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; }
|
.ws-support { margin-top: 8px; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; }
|
||||||
.ws-support b { color: var(--text); font-weight: 500; }
|
|
||||||
.ws-acts { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 8px; font-size: 13px; }
|
.ws-acts { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 8px; font-size: 13px; }
|
||||||
.ws-time { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
.ws-time { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||||
.ws-time .t { font-weight: 600; }
|
.ws-time .t { font-weight: 600; }
|
||||||
.ws-time .r { color: var(--text-2); font-size: 13px; }
|
|
||||||
.ws.comment .ws-text { color: var(--text-2); }
|
.ws.comment .ws-text { color: var(--text-2); }
|
||||||
button.link, .abo-toggle { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
|
button.link, .abo-toggle { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
|
||||||
button.link { font-weight: 500; } /* font: inherit setzt sonst das Gewicht von .link zurueck */
|
button.link { font-weight: 500; } /* font: inherit setzt sonst das Gewicht von .link zurueck */
|
||||||
@@ -996,6 +993,8 @@ button.link { font-weight: 500; } /* font: inherit setzt sonst das Gewicht von .
|
|||||||
/* Auf der markierten Zeile (--bg-subtle) reicht das helle Grau von .id und .faint nicht fuer den Kontrast */
|
/* Auf der markierten Zeile (--bg-subtle) reicht das helle Grau von .id und .faint nicht fuer den Kontrast */
|
||||||
.table tr.row:hover .id, .table tr.row:hover .faint { color: var(--text-2); }
|
.table tr.row:hover .id, .table tr.row:hover .faint { color: var(--text-2); }
|
||||||
.list-more { display: flex; justify-content: center; padding: 8px 0 24px; }
|
.list-more { display: flex; justify-content: center; padding: 8px 0 24px; }
|
||||||
|
/* Der lange Platzhalter wird am Handy mit "…" gekuerzt statt hart abgeschnitten */
|
||||||
|
.toolbar .search { text-overflow: ellipsis; }
|
||||||
@media (min-width: 861px) {
|
@media (min-width: 861px) {
|
||||||
.toolbar .search { width: 400px; }
|
.toolbar .search { width: 400px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ test('ticketPath: Sonderzeichen kodiert, leeres ref ergibt /tickets', () => {
|
|||||||
test('ticketRef: woid vor $id, nie die Kurznummer', () => {
|
test('ticketRef: woid vor $id, nie die Kurznummer', () => {
|
||||||
assert.equal(ticketRef({ woid: '100131', $id: 'abc' }), '100131')
|
assert.equal(ticketRef({ woid: '100131', $id: 'abc' }), '100131')
|
||||||
assert.equal(ticketRef({ woid: '', $id: 'abcdefghij' }), 'abcdefghij')
|
assert.equal(ticketRef({ woid: '', $id: 'abcdefghij' }), 'abcdefghij')
|
||||||
|
assert.equal(ticketRef({ woid: ' ', $id: 'abcdefghij' }), 'abcdefghij')
|
||||||
assert.equal(ticketRef(null), '')
|
assert.equal(ticketRef(null), '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user