Projekte: Zuordnen-Assistent mit Abschnitten, Brotkrumen und im Text markierter Auswahl
Der Assistent bekommt Brotkrume und Untertitel im Seitenkopf, je Schritt einen Abschnitt, Auswahlzeilen als Knoepfe mit aria-pressed und dem Badge Ausgewaehlt, Felder mit Label-Verknuepfung und keine Icons oder Inline-Stile mehr. Logik, Zustaende und Texte bleiben unveraendert. Vier CSS-Zeilen im Block Projekte (P5) fuer Abstand der Schrittspalte und Kontrast der Kundennummer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
860079f51d
commit
8b688f9df9
2 files changed
+194
-169
No files matched your search
+189
-169
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { FaSpinner, FaPlus, FaArrowLeft, FaArrowRight, FaCheck } from 'react-icons/fa6'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
@@ -9,7 +8,7 @@ import { nextStep, prevStep, newTicketDefaults, canFinish, openTicketsFor } from
|
||||
import { suggestSubdomain, slugify } from '../lib/subdomain'
|
||||
import { isArchived } from '../lib/leads'
|
||||
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
|
||||
import { PageHeader, Card, Button, Field } from '../components/ui'
|
||||
import { PageHeader, Card, Button, Field, Badge } from '../components/ui'
|
||||
import { CreateCustomerForm } from './CustomersPage'
|
||||
|
||||
const todayIso = () => new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' })
|
||||
@@ -176,36 +175,45 @@ export default function AssignProjectPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="page"><div className="empty"><FaSpinner className="spinner" /></div></div>
|
||||
if (loading) return <div className="page"><div className="loading">Wird geladen …</div></div>
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Projekt zuordnen" />
|
||||
<Card><div className="text-red">{loadError}</div></Card>
|
||||
<PageHeader crumb={[{ label: 'Projekte', to: '/projects' }]} title="Projekt zuordnen" />
|
||||
<p className="msg err" role="alert">{loadError}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createdLine = project.$createdAt && (
|
||||
<> · angelegt am {fmtDate(project.$createdAt)}{project.createdByGitea ? ` von ${project.createdByGitea}` : ''}</>
|
||||
)
|
||||
const header = (
|
||||
<Card>
|
||||
<div style={{ fontWeight: 700, fontSize: 18 }}>{repoName || project.projectName}</div>
|
||||
<div className="muted" style={{ marginTop: 4 }}>
|
||||
{repoUrl && <a href={repoUrl} target="_blank" rel="noreferrer">Repository in Gitea öffnen</a>}
|
||||
{project.$createdAt && <> · angelegt am {fmtDate(project.$createdAt)}{project.createdByGitea ? ` von ${project.createdByGitea}` : ''}</>}
|
||||
</div>
|
||||
</Card>
|
||||
<PageHeader
|
||||
crumb={[{ label: 'Projekte', to: '/projects' }, { label: repoName || project.projectName }]}
|
||||
title="Projekt zuordnen"
|
||||
subtitle={(repoUrl || createdLine) ? (
|
||||
<>
|
||||
{repoUrl && <a className="link" href={repoUrl} target="_blank" rel="noreferrer">Repository in Gitea öffnen</a>}
|
||||
{createdLine}
|
||||
</>
|
||||
) : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Projekt zuordnen" />
|
||||
{header}
|
||||
<Card title="Zugeordnet, mit Hinweisen">
|
||||
<ul style={{ marginTop: 0, paddingLeft: 18 }}>
|
||||
{done.warnings.map((w, i) => <li key={i}>{w}</li>)}
|
||||
</ul>
|
||||
<Button variant="primary" onClick={() => navigate(done.path)}>Weiter <FaArrowRight /></Button>
|
||||
<div className="assign-step">
|
||||
<ul className="summary">
|
||||
{done.warnings.map((w, i) => <li key={i}>{w}</li>)}
|
||||
</ul>
|
||||
<div className="row-buttons">
|
||||
<Button type="button" variant="primary" onClick={() => navigate(done.path)}>Weiter</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
@@ -215,16 +223,15 @@ export default function AssignProjectPage() {
|
||||
if (project.kind) {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Projekt zuordnen" />
|
||||
{header}
|
||||
<Card>
|
||||
{notice && <div className="text-red" style={{ marginBottom: 8 }}>{notice}</div>}
|
||||
<p style={{ marginTop: 0 }}>
|
||||
Bereits erledigt von <strong>{project.kindSetBy || 'unbekannt'}</strong> am {fmtDate(project.kindSetAt)}.
|
||||
</p>
|
||||
{project.ticketId && (
|
||||
<p style={{ marginBottom: 0 }}><TicketLink ticketId={project.ticketId} /></p>
|
||||
)}
|
||||
<div className="assign-step">
|
||||
{notice && <p className="msg err" role="alert">{notice}</p>}
|
||||
<p>
|
||||
Bereits erledigt von <strong>{project.kindSetBy || 'unbekannt'}</strong> am {fmtDate(project.kindSetAt)}.
|
||||
</p>
|
||||
{project.ticketId && <p><TicketLink ticketId={project.ticketId} /></p>}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
@@ -236,159 +243,172 @@ export default function AssignProjectPage() {
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Projekt zuordnen" subtitle={STEP_TITLES[state.step]} />
|
||||
{header}
|
||||
|
||||
<Card title={STEP_TITLES[state.step]}>
|
||||
{state.step === 'kind' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 10 }}>
|
||||
{kinds.map((k) => (
|
||||
<Button
|
||||
key={k.key}
|
||||
variant={state.kind === k.key ? 'primary' : 'default'}
|
||||
style={{ padding: '18px 12px', fontSize: 16, fontWeight: 700 }}
|
||||
onClick={() => patch({ kind: k.key, newTicket: null, ticketId: '' })}
|
||||
>
|
||||
{k.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.step === 'customer?' && (
|
||||
<div className="flex gap-2 wrap">
|
||||
<Button variant="primary" style={{ padding: '14px 28px' }}
|
||||
onClick={() => { patch({ forCustomer: true }); go('customer') }}>Ja, für einen Kunden</Button>
|
||||
<Button style={{ padding: '14px 28px' }}
|
||||
onClick={() => { patch({ forCustomer: false, customerId: '', ticketId: '', newTicket: null }); go('summary') }}>Nein, internes Projekt</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.step === 'customer' && (
|
||||
<>
|
||||
<div className="flex gap-2 wrap" style={{ marginBottom: 10 }}>
|
||||
<input className="form-control search-input" placeholder="Kunde suchen …" value={search}
|
||||
onChange={(e) => setSearch(e.target.value)} style={{ maxWidth: 320 }} />
|
||||
{isAdmin && (
|
||||
<Button onClick={() => setShowNewCustomer((s) => !s)}><FaPlus /> Neuer Kunde</Button>
|
||||
)}
|
||||
</div>
|
||||
{showNewCustomer && isAdmin && (
|
||||
<CreateCustomerForm
|
||||
onCancel={() => setShowNewCustomer(false)}
|
||||
onCreate={async (data) => {
|
||||
const r = await createCustomer(data)
|
||||
if (r.success) {
|
||||
setShowNewCustomer(false)
|
||||
patch({ customerId: r.data.$id, ticketId: '', newTicket: null })
|
||||
}
|
||||
return r
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="list" style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{filteredCustomers.slice(0, 100).map((c) => (
|
||||
<div key={c.$id} className="list-row" style={{ cursor: 'pointer', background: state.customerId === c.$id ? 'var(--surface-2)' : undefined }}
|
||||
onClick={() => patch({ customerId: c.$id, ticketId: '', newTicket: null })}>
|
||||
<div className="grow">
|
||||
<div style={{ fontWeight: 700 }}>{c.name}{c.code && <span className="faint" style={{ fontWeight: 400 }}> · {c.code}</span>}</div>
|
||||
<div className="muted">{[c.location, c.email].filter(Boolean).join(' · ') || '-'}</div>
|
||||
</div>
|
||||
{state.customerId === c.$id && <FaCheck />}
|
||||
</div>
|
||||
<div className="assign-step">
|
||||
{state.step === 'kind' && (
|
||||
<div className="seg wrap" role="group" aria-label={STEP_TITLES.kind}>
|
||||
{kinds.map((k) => (
|
||||
<button
|
||||
key={k.key}
|
||||
type="button"
|
||||
aria-pressed={state.kind === k.key}
|
||||
onClick={() => patch({ kind: k.key, newTicket: null, ticketId: '' })}
|
||||
>
|
||||
{k.label}
|
||||
</button>
|
||||
))}
|
||||
{filteredCustomers.length === 0 && <div className="muted" style={{ padding: 12 }}>Kein Kunde gefunden.</div>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{state.step === 'ticket' && (
|
||||
<>
|
||||
{ticketsLoading ? <FaSpinner className="spinner" /> : ticketsError ? (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="text-red" style={{ marginBottom: 8 }}>Die Tickets des Kunden konnten nicht geladen werden.</div>
|
||||
<Button onClick={() => setTicketsReload((n) => n + 1)}>Erneut versuchen</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="list" style={{ marginBottom: 12 }}>
|
||||
{openTickets.map((t) => (
|
||||
<div key={t.$id} className="list-row" style={{ cursor: 'pointer', background: state.ticketId === t.$id ? 'var(--surface-2)' : undefined }}
|
||||
onClick={() => patch({ ticketId: t.$id, newTicket: null })}>
|
||||
<div className="grow">
|
||||
<div style={{ fontWeight: 700 }}>#{t.woid} {t.topic}</div>
|
||||
<div className="muted">{[t.type, t.status].filter(Boolean).join(' · ')}</div>
|
||||
</div>
|
||||
{state.ticketId === t.$id && <FaCheck />}
|
||||
</div>
|
||||
))}
|
||||
{openTickets.length === 0 && <div className="muted" style={{ padding: 12 }}>Dieser Kunde hat keine offenen Tickets.</div>}
|
||||
</div>
|
||||
)}
|
||||
{!ticketsError && !ticketsLoading && (
|
||||
<Button variant={state.newTicket ? 'primary' : 'default'} onClick={startNewTicket}><FaPlus /> Neues Ticket anlegen</Button>
|
||||
)}
|
||||
{state.newTicket && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div className="flex gap-3 wrap">
|
||||
<Field label="Thema"><input className="form-control" value={state.newTicket.topic} onChange={setNT('topic')} /></Field>
|
||||
<Field label="Angefragt von"><input className="form-control" value={state.newTicket.requestedBy} onChange={setNT('requestedBy')} /></Field>
|
||||
<Field label="Start"><input className="form-control" type="date" value={state.newTicket.startDate} onChange={setNT('startDate')} /></Field>
|
||||
</div>
|
||||
<Field label="Details"><textarea className="form-control" rows={3} value={state.newTicket.details} onChange={setNT('details')} /></Field>
|
||||
<div className="muted" style={{ fontSize: 13 }}>Ticket-Art: {kindObj?.ticketType === 'Migration' ? 'Migration' : 'Webpage'} (ergibt sich aus der Art)</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{state.step === 'customer?' && (
|
||||
<div className="row-buttons">
|
||||
<Button type="button" variant="primary"
|
||||
onClick={() => { patch({ forCustomer: true }); go('customer') }}>Ja, für einen Kunden</Button>
|
||||
<Button type="button"
|
||||
onClick={() => { patch({ forCustomer: false, customerId: '', ticketId: '', newTicket: null }); go('summary') }}>Nein, internes Projekt</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.step === 'summary' && (
|
||||
<>
|
||||
<ul style={{ marginTop: 0, paddingLeft: 18 }}>
|
||||
<li>Art: <strong>{kindObj?.label}</strong></li>
|
||||
<li>{state.forCustomer ? <>Kunde: <strong>{customer?.name}</strong></> : 'Internes Projekt (ohne Kunde)'}</li>
|
||||
{state.forCustomer && (
|
||||
<li>
|
||||
{state.ticketId
|
||||
? <>Ticket: <strong>{openTickets.find((t) => t.$id === state.ticketId)?.topic || 'vorhandenes Ticket'}</strong></>
|
||||
: <>Neues Ticket: <strong>{state.newTicket?.topic}</strong></>}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{isWebsiteWithCustomer && hasPreviewAlready && (
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
{['deployed', 'ready'].includes(String(project.status || '').toLowerCase())
|
||||
? <>Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert.</>
|
||||
: <>Preview ist unter https://{project.subdomain}.project.webklar.com eingerichtet (Status: {project.status || 'unbekannt'}). Neu bauen geht im Projekt über „Preview".</>}
|
||||
</p>
|
||||
)}
|
||||
{isWebsiteWithCustomer && !hasPreviewAlready && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label className="flex gap-2" style={{ alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={previewOn} onChange={(e) => setPreviewOn(e.target.checked)} />
|
||||
Preview einschalten
|
||||
</label>
|
||||
{previewOn && (
|
||||
<Field label="Subdomain" hint={`Ergebnis: https://${slugify(subdomain) || '<subdomain>'}.project.webklar.com`}>
|
||||
<input className="form-control" value={subdomain} onChange={(e) => setSubdomain(e.target.value)} />
|
||||
</Field>
|
||||
{state.step === 'customer' && (
|
||||
<>
|
||||
<div className="row-buttons">
|
||||
<input className="input search" aria-label="Kunde suchen" placeholder="Kunde suchen …" value={search}
|
||||
onChange={(e) => setSearch(e.target.value)} />
|
||||
{isAdmin && (
|
||||
<Button type="button" onClick={() => setShowNewCustomer((s) => !s)}>Neuer Kunde</Button>
|
||||
)}
|
||||
</div>
|
||||
{showNewCustomer && isAdmin && (
|
||||
<CreateCustomerForm
|
||||
onCancel={() => setShowNewCustomer(false)}
|
||||
onCreate={async (data) => {
|
||||
const r = await createCustomer(data)
|
||||
if (r.success) {
|
||||
setShowNewCustomer(false)
|
||||
patch({ customerId: r.data.$id, ticketId: '', newTicket: null })
|
||||
}
|
||||
return r
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="pick-list">
|
||||
{filteredCustomers.slice(0, 100).map((c) => {
|
||||
const picked = state.customerId === c.$id
|
||||
return (
|
||||
<button key={c.$id} type="button" className="list-row" aria-pressed={picked}
|
||||
onClick={() => patch({ customerId: c.$id, ticketId: '', newTicket: null })}>
|
||||
<span className="list-row-text">
|
||||
<span className="cell-title">{c.name}{c.code && <span className="faint"> · {c.code}</span>}</span>
|
||||
<span className="cell-sub">{[c.location, c.email].filter(Boolean).join(' · ') || '-'}</span>
|
||||
</span>
|
||||
{picked && <Badge>Ausgewählt</Badge>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{filteredCustomers.length === 0 && <p className="muted pick-empty">Kein Kunde gefunden.</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.step === 'ticket' && (
|
||||
<>
|
||||
{ticketsLoading ? <p className="muted">Wird geladen …</p> : ticketsError ? (
|
||||
<>
|
||||
<p className="msg err" role="alert">Die Tickets des Kunden konnten nicht geladen werden.</p>
|
||||
<div className="row-buttons">
|
||||
<Button type="button" onClick={() => setTicketsReload((n) => n + 1)}>Erneut versuchen</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="pick-list">
|
||||
{openTickets.map((t) => {
|
||||
const picked = state.ticketId === t.$id
|
||||
return (
|
||||
<button key={t.$id} type="button" className="list-row" aria-pressed={picked}
|
||||
onClick={() => patch({ ticketId: t.$id, newTicket: null })}>
|
||||
<span className="list-row-text">
|
||||
<span className="cell-title">#{t.woid} {t.topic}</span>
|
||||
<span className="cell-sub">{[t.type, t.status].filter(Boolean).join(' · ')}</span>
|
||||
</span>
|
||||
{picked && <Badge>Ausgewählt</Badge>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{openTickets.length === 0 && <p className="muted pick-empty">Dieser Kunde hat keine offenen Tickets.</p>}
|
||||
</div>
|
||||
)}
|
||||
{!ticketsError && !ticketsLoading && (
|
||||
<div className="row-buttons">
|
||||
<Button type="button" variant={state.newTicket ? 'primary' : 'default'} onClick={startNewTicket}>Neues Ticket anlegen</Button>
|
||||
</div>
|
||||
)}
|
||||
{state.newTicket && (
|
||||
<>
|
||||
<div className="field-row-3">
|
||||
<Field label="Thema" htmlFor="assign-topic"><input id="assign-topic" className="input" value={state.newTicket.topic} onChange={setNT('topic')} /></Field>
|
||||
<Field label="Angefragt von" htmlFor="assign-requested-by"><input id="assign-requested-by" className="input" value={state.newTicket.requestedBy} onChange={setNT('requestedBy')} /></Field>
|
||||
<Field label="Start" htmlFor="assign-start"><input id="assign-start" className="input" type="date" value={state.newTicket.startDate} onChange={setNT('startDate')} /></Field>
|
||||
</div>
|
||||
<Field label="Details" htmlFor="assign-details"><textarea id="assign-details" className="textarea" rows={3} value={state.newTicket.details} onChange={setNT('details')} /></Field>
|
||||
<p className="muted small">Ticket-Art: {kindObj?.ticketType === 'Migration' ? 'Migration' : 'Webpage'} (ergibt sich aus der Art)</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.step === 'summary' && (
|
||||
<>
|
||||
<ul className="summary">
|
||||
<li>Art: <strong>{kindObj?.label}</strong></li>
|
||||
<li>{state.forCustomer ? <>Kunde: <strong>{customer?.name}</strong></> : 'Internes Projekt (ohne Kunde)'}</li>
|
||||
{state.forCustomer && (
|
||||
<li>
|
||||
{state.ticketId
|
||||
? <>Ticket: <strong>{openTickets.find((t) => t.$id === state.ticketId)?.topic || 'vorhandenes Ticket'}</strong></>
|
||||
: <>Neues Ticket: <strong>{state.newTicket?.topic}</strong></>}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{isWebsiteWithCustomer && hasPreviewAlready && (
|
||||
<p className="muted">
|
||||
{['deployed', 'ready'].includes(String(project.status || '').toLowerCase())
|
||||
? <>Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert.</>
|
||||
: <>Preview ist unter https://{project.subdomain}.project.webklar.com eingerichtet (Status: {project.status || 'unbekannt'}). Neu bauen geht im Projekt über „Preview".</>}
|
||||
</p>
|
||||
)}
|
||||
{isWebsiteWithCustomer && !hasPreviewAlready && (
|
||||
<>
|
||||
<label className="check">
|
||||
<input type="checkbox" checked={previewOn} onChange={(e) => setPreviewOn(e.target.checked)} />
|
||||
Preview einschalten
|
||||
</label>
|
||||
{previewOn && (
|
||||
<Field label="Subdomain" htmlFor="assign-subdomain" hint={`Ergebnis: https://${slugify(subdomain) || '<subdomain>'}.project.webklar.com`}>
|
||||
<input id="assign-subdomain" className="input" value={subdomain} onChange={(e) => setSubdomain(e.target.value)} />
|
||||
</Field>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="row-buttons">
|
||||
<Button type="button" variant="primary" disabled={busy || !canFinish(state) || (isWebsiteWithCustomer && !hasPreviewAlready && previewOn && !slugify(subdomain))} onClick={finish}>
|
||||
{busy ? 'Speichert …' : 'Abschließen'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <p className="msg err" role="alert">{error}</p>}
|
||||
|
||||
<div className="row-buttons">
|
||||
{state.step !== 'kind' && (
|
||||
<Button type="button" variant="ghost" disabled={busy} onClick={() => go(prevStep(state))}>Zurück</Button>
|
||||
)}
|
||||
<Button variant="primary" disabled={busy || !canFinish(state) || (isWebsiteWithCustomer && !hasPreviewAlready && previewOn && !slugify(subdomain))} onClick={finish}>
|
||||
{busy ? <FaSpinner className="spinner" /> : <><FaCheck /> Abschließen</>}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <div className="text-red" style={{ marginTop: 10 }}>{error}</div>}
|
||||
|
||||
<div className="flex gap-2" style={{ marginTop: 16 }}>
|
||||
{state.step !== 'kind' && (
|
||||
<Button variant="ghost" disabled={busy} onClick={() => go(prevStep(state))}><FaArrowLeft /> Zurück</Button>
|
||||
)}
|
||||
{(state.step === 'kind' || state.step === 'customer' || state.step === 'ticket') && (
|
||||
<Button variant="primary" disabled={!stepReady()} onClick={() => go(nextStep(state))}>Weiter <FaArrowRight /></Button>
|
||||
)}
|
||||
{(state.step === 'kind' || state.step === 'customer' || state.step === 'ticket') && (
|
||||
<Button type="button" variant="primary" disabled={!stepReady()} onClick={() => go(nextStep(state))}>Weiter</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -401,5 +421,5 @@ function TicketLink({ ticketId }) {
|
||||
databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, ticketId)
|
||||
.then((t) => setWoid(t.woid)).catch(() => setWoid(null))
|
||||
}, [ticketId])
|
||||
return woid ? <Link to={ticketPath(woid)}>Zum Ticket #{woid}</Link> : null
|
||||
return woid ? <Link className="link" to={ticketPath(woid)}>Zum Ticket #{woid}</Link> : null
|
||||
}
|
||||
@@ -1183,6 +1183,11 @@ button.list-row:hover, button.list-row[aria-pressed="true"] { background: var(--
|
||||
button.list-row .cell-sub { display: block; }
|
||||
button.list-row:focus-visible { outline-offset: -2px; } /* in .pick-list (Scroll-Container) wuerde der Ring sonst abgeschnitten */
|
||||
.summary { list-style: none; display: flex; flex-direction: column; gap: 6px; }
|
||||
/* Inhalt eines Schritts als Spalte mit Abstand (der Reset setzt jeden Aussenabstand auf 0); Segment und Haken bleiben so breit wie ihr Inhalt */
|
||||
.assign-step { display: flex; flex-direction: column; gap: 12px; }
|
||||
.assign-step > .seg, .assign-step > .check { align-self: flex-start; }
|
||||
.page .assign-step .field { margin-bottom: 0; } /* KOMPAT gibt .page .field 12 px unten; hier steht der Abstand der Spalte (P10 loescht beide) */
|
||||
button.list-row:hover .faint, button.list-row[aria-pressed="true"] .faint { color: var(--text-2); } /* .faint hat auf --bg-hover nur 2,9:1 (Muster: .table tr.row:hover .faint) */
|
||||
/* ===== ENDE Projekte (P5) ===== */
|
||||
|
||||
/* ===== KOMPAT (bis P10) ===== */
|
||||
|
||||
Reference in new issue
Block a user