Kunden-Reiter Funktionen als Tabelle mit Textaktionen, Bestaetigung als eigene Zeile
Alle Funktionen als eine Tabelle: Projektzeile, Funktionszeilen mit Status, Ticket (ticketPath) und Textaktionen, Bestaetigung, Einbauanleitung und Zustimmung in Textform als Zeilen darunter, Webshop-Anfrage als Block unter der Tabelle. Alle .funktionen-*-Regeln aus admin-areas.css in den Block Kunden (P4), Handy mit Zweispalten-Zeilen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8ef47713b5
commit
8c34e932c1
4 files changed
+232
-185
No files matched your search
@@ -1,6 +1,5 @@
|
|||||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { FaCheck, FaCopy, FaRotateRight, FaSpinner } from 'react-icons/fa6'
|
|
||||||
import { Card, Badge, Button, EmptyState } from '../ui'
|
import { Card, Badge, Button, EmptyState } from '../ui'
|
||||||
import { copyText } from '../CopyableCredential'
|
import { copyText } from '../CopyableCredential'
|
||||||
import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi'
|
import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi'
|
||||||
@@ -11,9 +10,9 @@ import {
|
|||||||
} from '../../lib/funktionenView'
|
} from '../../lib/funktionenView'
|
||||||
import { CHANNELS, channelLabel } from '../../lib/analyseView'
|
import { CHANNELS, channelLabel } from '../../lib/analyseView'
|
||||||
import { berlinYmd, fmtDateTime } from '../../lib/format'
|
import { berlinYmd, fmtDateTime } from '../../lib/format'
|
||||||
|
import { ticketPath } from '../../lib/ticketRoutes'
|
||||||
import { zeitenStatusText, mergeTeam, abschaltMeldung } from '../../lib/zeitenView'
|
import { zeitenStatusText, mergeTeam, abschaltMeldung } from '../../lib/zeitenView'
|
||||||
import ZeitenPanel from './ZeitenPanel'
|
import ZeitenPanel from './ZeitenPanel'
|
||||||
import '../../styles/admin-areas.css'
|
|
||||||
|
|
||||||
// Reiter "Funktionen": News-Pinnwand, Terminplaner und Webshop je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen")
|
// Reiter "Funktionen": News-Pinnwand, Terminplaner und Webshop je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen")
|
||||||
const short = (text) => (text.length > 60 ? `${text.slice(0, 60)}…` : text)
|
const short = (text) => (text.length > 60 ? `${text.slice(0, 60)}…` : text)
|
||||||
@@ -30,13 +29,13 @@ function SiteKey({ value }) {
|
|||||||
timer.current = setTimeout(() => setCopied(false), 1800)
|
timer.current = setTimeout(() => setCopied(false), 1800)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="funktionen-sitekey">
|
<div className="funktionen-sitekey">
|
||||||
<span className="area-muted">Site-Key</span>
|
<span className="muted">Site-Key</span>
|
||||||
<code>{value}</code>
|
<code>{value}</code>
|
||||||
<button type="button" className="funktionen-sitekey-copy" onClick={copy} title={copied ? 'Kopiert' : 'Site-Key kopieren'} aria-label={copied ? 'Site-Key kopiert' : 'Site-Key kopieren'}>
|
<button type="button" className="link" onClick={copy} title={copied ? 'Kopiert' : 'Site-Key kopieren'} aria-label={copied ? 'Site-Key kopiert' : 'Site-Key kopieren'}>
|
||||||
{copied ? <FaCheck className="funktionen-sitekey-ok" /> : <FaCopy />}
|
{copied ? 'Kopiert' : 'Kopieren'}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +51,8 @@ function KopierKnopf({ text, label }) {
|
|||||||
timer.current = setTimeout(() => setCopied(false), 1800)
|
timer.current = setTimeout(() => setCopied(false), 1800)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button size="sm" variant="ghost" onClick={copy} title={label}>
|
<Button type="button" onClick={copy}>
|
||||||
{copied ? <><FaCheck className="funktionen-sitekey-ok" /> Kopiert</> : <><FaCopy /> {label}</>}
|
{copied ? 'Kopiert' : label}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,31 +77,31 @@ function AnleitungPanel({ customerId, projectId, feature, reloadKey }) {
|
|||||||
const hindernis = hindernisText(feature.hindernis)
|
const hindernis = hindernisText(feature.hindernis)
|
||||||
if (fehler || !antwort) {
|
if (fehler || !antwort) {
|
||||||
return (
|
return (
|
||||||
<div className="area-row funktionen-panel">
|
<div className="funktionen-panel">
|
||||||
{fehler ? <div className="area-error">{fehler}</div> : <FaSpinner className="spinner" />}
|
{fehler ? <div className="msg err" role="alert">{fehler}</div> : <div className="muted">Wird geladen …</div>}
|
||||||
{hindernis && <div className="area-summary warn">{hindernis}</div>}
|
{hindernis && <div className="msg warn">{hindernis}</div>}
|
||||||
{fehler && <div><Button size="sm" variant="ghost" onClick={() => setRetry((n) => n + 1)}><FaRotateRight /> Erneut laden</Button></div>}
|
{fehler && <div><Button type="button" onClick={() => setRetry((n) => n + 1)}>Erneut laden</Button></div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modell = panelModell(antwort, feature)
|
const modell = panelModell(antwort, feature)
|
||||||
return (
|
return (
|
||||||
<div className="area-row funktionen-panel">
|
<div className="funktionen-panel">
|
||||||
<div className="funktionen-panel-head">
|
<div className="funktionen-panel-head">
|
||||||
<strong>Einbauanleitung</strong>
|
<strong>Einbauanleitung</strong>
|
||||||
{modell.kopf && <span className="area-muted">{modell.kopf}</span>}
|
{modell.kopf && <span className="muted small">{modell.kopf}</span>}
|
||||||
{modell.ticketPfad && <Link to={modell.ticketPfad}>Zum Ticket</Link>}
|
{antwort.ticketWoid && <Link className="link" to={ticketPath(antwort.ticketWoid)}>Zum Ticket</Link>}
|
||||||
<Button size="sm" variant="ghost" onClick={() => setRetry((n) => n + 1)} title="Anleitung neu laden" aria-label="Anleitung neu laden"><FaRotateRight /></Button>
|
<button type="button" className="link" onClick={() => setRetry((n) => n + 1)}>Anleitung neu laden</button>
|
||||||
</div>
|
</div>
|
||||||
{modell.warnungen.length > 0 && (
|
{modell.warnungen.length > 0 && (
|
||||||
<ul className="area-error funktionen-panel-warnungen">
|
<ul className="msg err funktionen-panel-warnungen">
|
||||||
{modell.warnungen.map((text, i) => <li key={i}>{text}</li>)}
|
{modell.warnungen.map((text, i) => <li key={i}>{text}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{modell.hinweis && <div className="area-summary warn">{modell.hinweis}</div>}
|
{modell.hinweis && <div className="msg warn">{modell.hinweis}</div>}
|
||||||
{modell.kopien.length > 0 && (
|
{modell.kopien.length > 0 && (
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
{modell.kopien.map((k) => <KopierKnopf key={k.id} text={k.text} label={k.label} />)}
|
{modell.kopien.map((k) => <KopierKnopf key={k.id} text={k.text} label={k.label} />)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -122,26 +121,26 @@ function TextformPanel({ funktion, feld, value, onChange, projectId, today, disa
|
|||||||
const ansicht = textformAnsicht(feld, funktion)
|
const ansicht = textformAnsicht(feld, funktion)
|
||||||
const { label, links, vorlage } = ansicht
|
const { label, links, vorlage } = ansicht
|
||||||
return (
|
return (
|
||||||
<div className="area-row funktionen-panel">
|
<div className="funktionen-panel">
|
||||||
<label className="area-inline" style={{ alignItems: 'flex-start', flexWrap: 'nowrap' }}>
|
<label className="funktionen-zustimmung">
|
||||||
<input type="checkbox" style={{ marginTop: 3 }} checked={value.confirmed} disabled={disabled} onChange={(e) => onChange({ ...value, confirmed: e.target.checked })} />
|
<input type="checkbox" checked={value.confirmed} disabled={disabled} onChange={(e) => onChange({ ...value, confirmed: e.target.checked })} />
|
||||||
<span>
|
<span>
|
||||||
{label.before}
|
{label.before}
|
||||||
{links.lb ? <a href={links.lb} target="_blank" rel="noopener noreferrer">{label.lb}</a> : label.lb}
|
{links.lb ? <a className="link" href={links.lb} target="_blank" rel="noopener noreferrer">{label.lb}</a> : label.lb}
|
||||||
{label.between}
|
{label.between}
|
||||||
{links.avv ? <a href={links.avv} target="_blank" rel="noopener noreferrer">{label.avv}</a> : label.avv}
|
{links.avv ? <a className="link" href={links.avv} target="_blank" rel="noopener noreferrer">{label.avv}</a> : label.avv}
|
||||||
{label.after}
|
{label.after}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
<label>
|
<label className="flex items-center gap-2 wrap">
|
||||||
Datum der Zustimmung:{' '}
|
Datum der Zustimmung:
|
||||||
<input type="date" required className="form-control" value={value.date} min={ansicht.version} max={today} disabled={disabled} onChange={(e) => onChange({ ...value, date: e.target.value })} />
|
<input type="date" required className="input" value={value.date} min={ansicht.version} max={today} disabled={disabled} onChange={(e) => onChange({ ...value, date: e.target.value })} />
|
||||||
</label>
|
</label>
|
||||||
<span role="radiogroup" aria-label="Weg der Zustimmung" className="area-inline">
|
<span role="radiogroup" aria-label="Weg der Zustimmung" className="flex items-center gap-2 wrap">
|
||||||
<span>Weg:</span>
|
<span>Weg:</span>
|
||||||
{CHANNELS.map((c) => (
|
{CHANNELS.map((c) => (
|
||||||
<label key={c} className="area-inline">
|
<label key={c} className="check">
|
||||||
<input type="radio" name={`textform-weg-${funktion}-${projectId}`} value={c} checked={value.channel === c} disabled={disabled} onChange={() => onChange({ ...value, channel: c })} />
|
<input type="radio" name={`textform-weg-${funktion}-${projectId}`} value={c} checked={value.channel === c} disabled={disabled} onChange={() => onChange({ ...value, channel: c })} />
|
||||||
{channelLabel(c)}
|
{channelLabel(c)}
|
||||||
</label>
|
</label>
|
||||||
@@ -149,13 +148,13 @@ function TextformPanel({ funktion, feld, value, onChange, projectId, today, disa
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{vorlage ? (
|
{vorlage ? (
|
||||||
<details>
|
<details className="funktionen-vorlage">
|
||||||
<summary style={{ cursor: 'pointer' }}>Textvorlage für den Chat</summary>
|
<summary>Textvorlage für den Chat</summary>
|
||||||
<p style={{ margin: '6px 0', whiteSpace: 'pre-wrap' }}>{vorlage}</p>
|
<p>{vorlage}</p>
|
||||||
<KopierKnopf text={vorlage} label="Vorlage kopieren" />
|
<KopierKnopf text={vorlage} label="Vorlage kopieren" />
|
||||||
</details>
|
</details>
|
||||||
) : (
|
) : (
|
||||||
<div className="area-summary warn">Die Adressen der Unterlagen fehlen; die Chat-Vorlage ist nicht verfügbar.</div>
|
<div className="msg warn">Die Adressen der Unterlagen fehlen; die Chat-Vorlage ist nicht verfügbar.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -190,7 +189,7 @@ function ShopPanel({ customerId, shop, onDecided }) {
|
|||||||
}
|
}
|
||||||
const ablehnenFehlt = frage === 'ablehnen' && !begruendungOk(begruendung)
|
const ablehnenFehlt = frage === 'ablehnen' && !begruendungOk(begruendung)
|
||||||
return (
|
return (
|
||||||
<div className="area-row funktionen-panel">
|
<div className="funktionen-shop">
|
||||||
<div className="funktionen-panel-head">
|
<div className="funktionen-panel-head">
|
||||||
<strong>Webshop: {shop.projectName}</strong>
|
<strong>Webshop: {shop.projectName}</strong>
|
||||||
{a && <Badge tone={tone}>Anfrage: {label}</Badge>}
|
{a && <Badge tone={tone}>Anfrage: {label}</Badge>}
|
||||||
@@ -198,34 +197,34 @@ function ShopPanel({ customerId, shop, onDecided }) {
|
|||||||
<Badge tone={stripeTon(stripe.status, !!stripe.accountId)}>Stripe: {stripeText(stripe.status, !!stripe.accountId)}</Badge>
|
<Badge tone={stripeTon(stripe.status, !!stripe.accountId)}>Stripe: {stripeText(stripe.status, !!stripe.accountId)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{a && (
|
{a && (
|
||||||
<dl className="area-kv">
|
<dl className="kv">
|
||||||
{anfrageZeilen(a.anfrage).map((z) => (
|
{anfrageZeilen(a.anfrage).map((z) => (
|
||||||
<Fragment key={z.label}><dt>{z.label}</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{z.text}</dd></Fragment>
|
<Fragment key={z.label}><dt>{z.label}</dt><dd>{z.text}</dd></Fragment>
|
||||||
))}
|
))}
|
||||||
<dt>Eingegangen</dt><dd>{a.at ? fmtDateTime(a.at) : '–'}</dd>
|
<dt>Eingegangen</dt><dd>{a.at ? fmtDateTime(a.at) : '–'}</dd>
|
||||||
{a.entschiedenAt && <><dt>Entschieden</dt><dd>{fmtDateTime(a.entschiedenAt)}</dd></>}
|
{a.entschiedenAt && <><dt>Entschieden</dt><dd>{fmtDateTime(a.entschiedenAt)}</dd></>}
|
||||||
{a.begruendung && <><dt>Begründung</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{a.begruendung}</dd></>}
|
{a.begruendung && <><dt>Begründung</dt><dd>{a.begruendung}</dd></>}
|
||||||
</dl>
|
</dl>
|
||||||
)}
|
)}
|
||||||
<div className="area-muted">Artikel: {shop.zahlen?.artikel ?? 0} · Offene Bestellungen: {shop.zahlen?.offeneBestellungen ?? 0}</div>
|
<div className="muted small">Artikel: {shop.zahlen?.artikel ?? 0} · Offene Bestellungen: {shop.zahlen?.offeneBestellungen ?? 0}</div>
|
||||||
{a?.status === 'offen' && (
|
{a?.status === 'offen' && (
|
||||||
frage ? (
|
frage ? (
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
<span>{frage === 'annehmen' ? 'Anfrage annehmen? Der Kunde bekommt eine Mail.' : 'Anfrage ablehnen? Der Kunde bekommt die Begründung per Mail.'}</span>
|
<span>{frage === 'annehmen' ? 'Anfrage annehmen? Der Kunde bekommt eine Mail.' : 'Anfrage ablehnen? Der Kunde bekommt die Begründung per Mail.'}</span>
|
||||||
{frage === 'ablehnen' && (
|
{frage === 'ablehnen' && (
|
||||||
<textarea className="form-control" required rows={3} maxLength={BEGRUENDUNG_MAX} placeholder="Begründung (Pflicht)" value={begruendung} onChange={(e) => setBegruendung(e.target.value)} style={{ width: '100%' }} />
|
<textarea className="textarea funktionen-begruendung" required rows={3} maxLength={BEGRUENDUNG_MAX} placeholder="Begründung (Pflicht)" aria-label="Begründung (Pflicht)" value={begruendung} onChange={(e) => setBegruendung(e.target.value)} />
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="primary" onClick={entscheiden} disabled={busy || ablehnenFehlt}>{busy ? <FaSpinner className="spinner" /> : 'Ja'}</Button>
|
<Button type="button" variant="primary" onClick={entscheiden} disabled={busy || ablehnenFehlt}>{busy ? 'Speichert …' : 'Ja'}</Button>
|
||||||
<Button size="sm" variant="ghost" onClick={() => setFrage(null)} disabled={busy}>Abbrechen</Button>
|
<Button type="button" onClick={() => setFrage(null)} disabled={busy}>Abbrechen</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
<Button size="sm" variant="primary" onClick={() => { setBegruendung(''); setFrage('annehmen') }}>Anfrage annehmen</Button>
|
<Button type="button" variant="primary" onClick={() => { setBegruendung(''); setFrage('annehmen') }}>Anfrage annehmen</Button>
|
||||||
<Button size="sm" variant="ghost" onClick={() => { setBegruendung(''); setFrage('ablehnen') }}>Ablehnen</Button>
|
<Button type="button" onClick={() => { setBegruendung(''); setFrage('ablehnen') }}>Ablehnen</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{status && <div className={status.ok ? 'area-message' : 'area-error'}>{status.text}</div>}
|
{status && <div className={`msg ${status.ok ? 'ok' : 'err'}`} role={status.ok ? undefined : 'alert'}>{status.text}</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -296,97 +295,122 @@ export default function CustomerFunktionen({ customerId }) {
|
|||||||
const panelOffen = (projectId) => (Object.prototype.hasOwnProperty.call(panel, projectId) ? panel[projectId] : anleitungOffen(window.location.search, projectId))
|
const panelOffen = (projectId) => (Object.prototype.hasOwnProperty.call(panel, projectId) ? panel[projectId] : anleitungOffen(window.location.search, projectId))
|
||||||
const togglePanel = (projectId) => setPanel((p) => ({ ...p, [projectId]: !panelOffen(projectId) }))
|
const togglePanel = (projectId) => setPanel((p) => ({ ...p, [projectId]: !panelOffen(projectId) }))
|
||||||
|
|
||||||
if (error) return <Card><div className="area-error">{error}</div></Card>
|
if (error) return <div className="msg err" role="alert">{error}</div>
|
||||||
if (!projects) return <Card><FaSpinner className="spinner" /></Card>
|
if (!projects) return <div className="loading">Wird geladen …</div>
|
||||||
if (!projects.length) return <Card><EmptyState title="Keine Projekte für diesen Kunden." /></Card>
|
if (!projects.length) return <EmptyState title="Keine Projekte für diesen Kunden." />
|
||||||
|
|
||||||
|
const shopEintraege = shop.filter(shopHatEintrag)
|
||||||
return (
|
return (
|
||||||
<Card title="Funktionen" pad={false}>
|
<Card title="Funktionen">
|
||||||
<div className="area-rows">
|
<table className="table funktionen-table">
|
||||||
<div className="area-row funktionen-row head"><span>Projekt</span><span>Funktion</span><span>Status</span><span>Ticket</span><span>Aktionen</span></div>
|
<thead>
|
||||||
{projects.flatMap((p) => FUNKTIONEN.map((f, index) => {
|
<tr><th>Funktion</th><th>Status</th><th>Ticket</th><th>Aktionen</th></tr>
|
||||||
const feature = funktionOf(p, f.key)
|
</thead>
|
||||||
const [tone, label] = ZUSTAND[feature.state] || ['muted', feature.state]
|
{projects.map((p) => (
|
||||||
const isConfirming = confirm?.projectId === p.projectId && confirm?.key === f.key
|
<tbody key={p.projectId}>
|
||||||
const hatAnleitung = f.key === 'termine' && feature.state !== 'off'
|
<tr className="funktionen-group">
|
||||||
const anleitungOffenHier = hatAnleitung && panelOffen(p.projectId)
|
<th colSpan={4} scope="rowgroup">
|
||||||
const braucheTextform = isConfirming && textformNoetig(f.key, confirm.action, felder[f.key])
|
{p.projectName}
|
||||||
const textformFehlt = braucheTextform && !textformBereit({ ...textform, today: heute }, felder[f.key])
|
{p.liveUrl && <>{' '}<a className="link" href={p.liveUrl} target="_blank" rel="noreferrer">{p.liveUrl.replace(/^https?:\/\//, '')}</a></>}
|
||||||
const hatZeitenPanel = f.key === 'zeiten' && feature.state !== 'off'
|
</th>
|
||||||
const zeitenText = f.key === 'zeiten' ? zeitenStatusText(feature) : ''
|
</tr>
|
||||||
return (
|
{FUNKTIONEN.map((f) => {
|
||||||
<Fragment key={`${p.projectId}:${f.key}`}>
|
const feature = funktionOf(p, f.key)
|
||||||
<div className="area-row funktionen-row">
|
const [tone, label] = ZUSTAND[feature.state] || ['muted', feature.state]
|
||||||
{index === 0 ? (
|
const isConfirming = confirm?.projectId === p.projectId && confirm?.key === f.key
|
||||||
<span>
|
const hatAnleitung = f.key === 'termine' && feature.state !== 'off'
|
||||||
<strong>{p.projectName}</strong>
|
const anleitungOffenHier = hatAnleitung && panelOffen(p.projectId)
|
||||||
{p.liveUrl && <><br /><a href={p.liveUrl} target="_blank" rel="noreferrer" className="area-muted">{p.liveUrl.replace(/^https?:\/\//, '')}</a></>}
|
const braucheTextform = isConfirming && textformNoetig(f.key, confirm.action, felder[f.key])
|
||||||
</span>
|
const textformFehlt = braucheTextform && !textformBereit({ ...textform, today: heute }, felder[f.key])
|
||||||
) : (
|
const siteKey = siteKeyOf(feature, f.key)
|
||||||
<span className="funktionen-leer" />
|
const hindernis = feature.state === 'setup' ? hindernisText(feature.hindernis) : ''
|
||||||
)}
|
const abgeschaltet = abgeschaltetText(feature)
|
||||||
<span>{f.label}</span>
|
const hatZeitenPanel = f.key === 'zeiten' && feature.state !== 'off'
|
||||||
<span>
|
const zeitenText = f.key === 'zeiten' ? zeitenStatusText(feature) : ''
|
||||||
<Badge tone={tone}>{label}</Badge>
|
// Zusatzzeilen direkt unter der Funktionszeile in dieser Reihenfolge: Bestaetigung, Zustimmung in Textform, Einbauanleitung.
|
||||||
{feature.state === 'setup' && hindernisText(feature.hindernis) && <><br /><span className="area-warn">{hindernisText(feature.hindernis)}</span></>}
|
// Alle Zeilen bis auf die letzte verlieren die Haarlinie, damit sie als eine Gruppe lesbar sind (.funktionen-offen).
|
||||||
{zeitenText && <><br /><span className={feature.hindernis ? 'area-warn' : 'area-muted'}>{zeitenText}</span></>}
|
const letzte = [isConfirming, braucheTextform, anleitungOffenHier, hatZeitenPanel].lastIndexOf(true)
|
||||||
{abgeschaltetText(feature) && <><br /><span className="area-muted">{abgeschaltetText(feature)}</span></>}
|
const offen = (nr) => nr < letzte
|
||||||
{feature.adminNote && <><br /><span className="area-muted" title={feature.adminNote}>{short(feature.adminNote)}</span></>}
|
return (
|
||||||
{siteKeyOf(feature, f.key) && <><br /><SiteKey value={siteKeyOf(feature, f.key)} /></>}
|
<Fragment key={f.key}>
|
||||||
</span>
|
<tr className={`funktionen-zeile${offen(-1) ? ' funktionen-offen' : ''}`}>
|
||||||
<span>
|
<td>{f.label}</td>
|
||||||
{feature.ticketMissing
|
<td>
|
||||||
? <Badge tone="danger">Ticket fehlt</Badge>
|
<Badge tone={tone}>{label}</Badge>
|
||||||
: feature.ticketWoid
|
{hindernis && <div className="cell-sub msg warn">{hindernis}</div>}
|
||||||
? <Link to={`/tickets?woid=${encodeURIComponent(feature.ticketWoid)}`}>#{feature.ticketWoid}</Link>
|
{zeitenText && <div className={feature.hindernis ? 'cell-sub msg warn' : 'cell-sub'}>{zeitenText}</div>}
|
||||||
: '–'}
|
{abgeschaltet && <div className="cell-sub">{abgeschaltet}</div>}
|
||||||
</span>
|
{feature.adminNote && <div className="cell-sub" title={feature.adminNote}>{short(feature.adminNote)}</div>}
|
||||||
<span className="area-inline">
|
{siteKey && <SiteKey value={siteKey} />}
|
||||||
{isConfirming ? (
|
</td>
|
||||||
<Fragment>
|
<td className={feature.ticketMissing || feature.ticketWoid ? undefined : 'funktionen-ohne-ticket'}>
|
||||||
<span>{f.bestaetigen[confirm.action]}</span>
|
{feature.ticketMissing
|
||||||
<input className="form-control" placeholder="Notiz (optional)" maxLength={120} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: 220 }} />
|
? <Badge tone="danger">Ticket fehlt</Badge>
|
||||||
<Button size="sm" variant="primary" onClick={save} disabled={busy || textformFehlt}>{busy ? <FaSpinner className="spinner" /> : 'Ja'}</Button>
|
: feature.ticketWoid
|
||||||
<Button size="sm" variant="ghost" onClick={() => setConfirm(null)} disabled={busy}>Abbrechen</Button>
|
? <Link className="link" to={ticketPath(feature.ticketWoid)}>#{feature.ticketWoid}</Link>
|
||||||
</Fragment>
|
: <span className="faint">–</span>}
|
||||||
) : (
|
</td>
|
||||||
<Fragment>
|
<td>
|
||||||
{aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing, hindernis: feature.hindernis }).map((action) => (
|
{!isConfirming && (
|
||||||
<Button key={action} size="sm" variant="ghost" onClick={() => ask(p.projectId, f.key, action)}>
|
<div className="funktionen-actions">
|
||||||
{AKTION_LABEL[action]}
|
{aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing, hindernis: feature.hindernis }).map((action) => (
|
||||||
</Button>
|
<button key={action} type="button" className="link" onClick={() => ask(p.projectId, f.key, action)}>
|
||||||
))}
|
{AKTION_LABEL[action]}
|
||||||
{hatAnleitung && (
|
</button>
|
||||||
<Button size="sm" variant="ghost" onClick={() => togglePanel(p.projectId)} aria-expanded={anleitungOffenHier}>
|
))}
|
||||||
Anleitung
|
{hatAnleitung && (
|
||||||
</Button>
|
<button type="button" className="link" onClick={() => togglePanel(p.projectId)} aria-expanded={anleitungOffenHier}>
|
||||||
|
Anleitung
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isConfirming && (
|
||||||
|
<tr className={`funktionen-confirm${offen(0) ? ' funktionen-offen' : ''}`}>
|
||||||
|
<td colSpan={4}>
|
||||||
|
<div className="flex items-center gap-2 wrap">
|
||||||
|
<span>{f.bestaetigen[confirm.action]}</span>
|
||||||
|
<input className="input funktionen-note" placeholder="Notiz (optional)" maxLength={120} aria-label="Notiz (optional)" value={note} onChange={(e) => setNote(e.target.value)} />
|
||||||
|
<Button type="button" variant="primary" onClick={save} disabled={busy || textformFehlt}>{busy ? 'Speichert …' : 'Ja'}</Button>
|
||||||
|
<Button type="button" onClick={() => setConfirm(null)} disabled={busy}>Abbrechen</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
)}
|
)}
|
||||||
</span>
|
{braucheTextform && (
|
||||||
</div>
|
<tr className={`funktionen-extra${offen(1) ? ' funktionen-offen' : ''}`}>
|
||||||
{braucheTextform && (
|
<td colSpan={4}>
|
||||||
<TextformPanel funktion={f.key} feld={felder[f.key]} value={textform} onChange={setTextform} projectId={p.projectId} today={heute} disabled={busy} />
|
<TextformPanel funktion={f.key} feld={felder[f.key]} value={textform} onChange={setTextform} projectId={p.projectId} today={heute} disabled={busy} />
|
||||||
)}
|
</td>
|
||||||
{hatZeitenPanel && (
|
</tr>
|
||||||
<ZeitenPanel customerId={customerId} projectId={p.projectId} feature={feature} onSaved={teamGespeichert} onReload={load} />
|
)}
|
||||||
)}
|
{anleitungOffenHier && (
|
||||||
{anleitungOffenHier && (
|
<tr className={`funktionen-extra${offen(2) ? ' funktionen-offen' : ''}`}>
|
||||||
<AnleitungPanel customerId={customerId} projectId={p.projectId} feature={feature} reloadKey={panelReload[p.projectId] || 0} />
|
<td colSpan={4}>
|
||||||
)}
|
<AnleitungPanel customerId={customerId} projectId={p.projectId} feature={feature} reloadKey={panelReload[p.projectId] || 0} />
|
||||||
</Fragment>
|
</td>
|
||||||
)
|
</tr>
|
||||||
}))}
|
)}
|
||||||
</div>
|
{hatZeitenPanel && (
|
||||||
{status && <div className={status.ok ? 'area-message' : 'area-error'} style={{ margin: 12 }}>{status.text}</div>}
|
<tr className="funktionen-extra">
|
||||||
{shopFehler && <div className="area-error" style={{ margin: 12 }}>{shopFehler}</div>}
|
<td colSpan={4}>
|
||||||
{shop.filter(shopHatEintrag).length > 0 && (
|
<ZeitenPanel customerId={customerId} projectId={p.projectId} feature={feature} onSaved={teamGespeichert} onReload={load} />
|
||||||
<div className="area-rows">
|
</td>
|
||||||
{shop.filter(shopHatEintrag).map((s) => (
|
</tr>
|
||||||
<ShopPanel key={s.projectId} customerId={customerId} shop={s} onDecided={nachEntscheidung} />
|
)}
|
||||||
))}
|
</Fragment>
|
||||||
</div>
|
)
|
||||||
)}
|
})}
|
||||||
|
</tbody>
|
||||||
|
))}
|
||||||
|
</table>
|
||||||
|
{status && <div className={`msg ${status.ok ? 'ok' : 'err'} funktionen-msg`} role={status.ok ? undefined : 'alert'}>{status.text}</div>}
|
||||||
|
{shopFehler && <div className="msg err funktionen-msg" role="alert">{shopFehler}</div>}
|
||||||
|
{shopEintraege.map((s) => (
|
||||||
|
<ShopPanel key={s.projectId} customerId={customerId} shop={s} onDecided={nachEntscheidung} />
|
||||||
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { FaCopy, FaSpinner } from 'react-icons/fa6'
|
|
||||||
import { Button } from '../ui'
|
import { Button } from '../ui'
|
||||||
import { getZeitenErsatz, patchZeitenTeam } from '../../lib/teamApi'
|
import { getZeitenErsatz, patchZeitenTeam } from '../../lib/teamApi'
|
||||||
import {
|
import {
|
||||||
@@ -9,8 +8,8 @@ import {
|
|||||||
|
|
||||||
// Panel "Öffnungszeiten live" unter der Zeile im Reiter "Funktionen": Team-Einstellungen der Website-Leisten (nur das Team ändert sie)
|
// Panel "Öffnungszeiten live" unter der Zeile im Reiter "Funktionen": Team-Einstellungen der Website-Leisten (nur das Team ändert sie)
|
||||||
// und "Ersatz-HTML kopieren" je Plan. Entscheidungen, Texte und Prüfungen stehen in zeitenView (rein, getestet); hier wird nur verdrahtet.
|
// und "Ersatz-HTML kopieren" je Plan. Entscheidungen, Texte und Prüfungen stehen in zeitenView (rein, getestet); hier wird nur verdrahtet.
|
||||||
// Texte stehen ausschließlich als React-Textknoten da (nie als HTML). Die Hülle (area-row funktionen-panel) liegt als eigene Zeile außerhalb
|
// Texte stehen ausschließlich als React-Textknoten da (nie als HTML). Die Hülle (funktionen-panel) steht in einer eigenen Tabellenzeile
|
||||||
// des Fünf-Spalten-Rasters, wie die Einbauanleitung des Terminplaners.
|
// unter der Funktionszeile, wie die Einbauanleitung des Terminplaners; weißes Design: keine Icons, Bausteine aus ../ui.
|
||||||
export default function ZeitenPanel({ customerId, projectId, feature, onSaved, onReload }) {
|
export default function ZeitenPanel({ customerId, projectId, feature, onSaved, onReload }) {
|
||||||
const start = teamFormular(feature)
|
const start = teamFormular(feature)
|
||||||
const [form, setForm] = useState(start)
|
const [form, setForm] = useState(start)
|
||||||
@@ -62,17 +61,17 @@ export default function ZeitenPanel({ customerId, projectId, feature, onSaved, o
|
|||||||
const knoepfe = ersatzKnoepfe(feature)
|
const knoepfe = ersatzKnoepfe(feature)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="area-row funktionen-panel funktionen-panel-zeiten">
|
<div className="funktionen-panel funktionen-panel-zeiten">
|
||||||
<div className="funktionen-panel-head">
|
<div className="funktionen-panel-head">
|
||||||
<strong>Öffnungszeiten live</strong>
|
<strong>Öffnungszeiten live</strong>
|
||||||
<span className="area-muted">Einstellungen des Teams (der Kunde sieht und ändert sie nicht)</span>
|
<span className="muted small">Einstellungen des Teams (der Kunde sieht und ändert sie nicht)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={speichern} className="funktionen-panel-form">
|
<form onSubmit={speichern} className="funktionen-panel-form">
|
||||||
<label className="funktionen-panel-feld">
|
<label className="funktionen-panel-feld">
|
||||||
<span>Header-Selektor</span>
|
<span>Header-Selektor</span>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="input"
|
||||||
value={form.navSelektor}
|
value={form.navSelektor}
|
||||||
onChange={(e) => aendern({ navSelektor: e.target.value })}
|
onChange={(e) => aendern({ navSelektor: e.target.value })}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
@@ -82,51 +81,51 @@ export default function ZeitenPanel({ customerId, projectId, feature, onSaved, o
|
|||||||
aria-invalid={selektorFehler ? 'true' : 'false'}
|
aria-invalid={selektorFehler ? 'true' : 'false'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="area-muted">
|
<div className="muted small">
|
||||||
Nur nötig, wenn die Hinweis-Leiste den festen Header der Website überlappt: CSS-Selektor des Headers.
|
Nur nötig, wenn die Hinweis-Leiste den festen Header der Website überlappt: CSS-Selektor des Headers.
|
||||||
</div>
|
</div>
|
||||||
{selektorFehler && <div className="area-error" role="alert">{selektorFehler}</div>}
|
{selektorFehler && <div className="msg err" role="alert">{selektorFehler}</div>}
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
<label className="area-inline">
|
<label className="check">
|
||||||
<input type="checkbox" checked={form.kontaktleisteAus} disabled={busy} onChange={(e) => aendern({ kontaktleisteAus: e.target.checked })} />
|
<input type="checkbox" checked={form.kontaktleisteAus} disabled={busy} onChange={(e) => aendern({ kontaktleisteAus: e.target.checked })} />
|
||||||
Kontakt-Leiste aus
|
Kontakt-Leiste aus
|
||||||
</label>
|
</label>
|
||||||
<label className="area-inline">
|
<label className="check">
|
||||||
<input type="checkbox" checked={form.hinweisleisteAus} disabled={busy} onChange={(e) => aendern({ hinweisleisteAus: e.target.checked })} />
|
<input type="checkbox" checked={form.hinweisleisteAus} disabled={busy} onChange={(e) => aendern({ hinweisleisteAus: e.target.checked })} />
|
||||||
Hinweis-Leiste aus
|
Hinweis-Leiste aus
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
<Button type="submit" size="sm" variant="primary" disabled={busy || Boolean(selektorFehler) || !geaendert}>
|
<Button type="submit" variant="primary" disabled={busy || Boolean(selektorFehler) || !geaendert}>
|
||||||
{busy ? <FaSpinner className="spinner" /> : 'Speichern'}
|
{busy ? 'Speichert …' : 'Speichern'}
|
||||||
</Button>
|
</Button>
|
||||||
{meldung && <span role="status" className={meldung.ok ? 'area-message funktionen-panel-meldung' : 'area-error funktionen-panel-meldung'}>{meldung.text}</span>}
|
{meldung && <span role="status" className={meldung.ok ? 'msg ok' : 'msg err'}>{meldung.text}</span>}
|
||||||
{meldung?.neuLaden && (
|
{meldung?.neuLaden && (
|
||||||
<Button type="button" size="sm" variant="ghost" onClick={() => { setMeldung(null); onReload() }}>Neu laden</Button>
|
<Button type="button" onClick={() => { setMeldung(null); onReload() }}>Neu laden</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="funktionen-panel-head">
|
<div className="funktionen-panel-head">
|
||||||
<strong>Ersatz-HTML</strong>
|
<strong>Ersatz-HTML</strong>
|
||||||
<span className="area-muted">Wochentabelle des Plans für den Platzhalter der Website (Einbau)</span>
|
<span className="muted small">Wochentabelle des Plans für den Platzhalter der Website (Einbau)</span>
|
||||||
</div>
|
</div>
|
||||||
{knoepfe.length === 0 ? (
|
{knoepfe.length === 0 ? (
|
||||||
<div className="area-muted">{KEINE_ZEITEN_TEXT}</div>
|
<div className="muted small">{KEINE_ZEITEN_TEXT}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="area-inline">
|
<div className="flex items-center gap-2 wrap">
|
||||||
{knoepfe.map((k) => (
|
{knoepfe.map((k) => (
|
||||||
<Button key={k.kurz} type="button" size="sm" variant="ghost" disabled={ersatz.busy} onClick={() => kopieren(k)} title={`Plan „${k.kurz}“`}>
|
<Button key={k.kurz} type="button" disabled={ersatz.busy} onClick={() => kopieren(k)} title={`Plan „${k.kurz}“`}>
|
||||||
{ersatz.busy && ersatz.kurz === k.kurz ? <FaSpinner className="spinner" /> : <FaCopy />} {k.label}
|
{ersatz.busy && ersatz.kurz === k.kurz ? 'Lädt …' : k.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ersatz.meldung && <div role="status" className={ersatz.meldung.ok ? 'area-message' : 'area-error'}>{ersatz.meldung.text}</div>}
|
{ersatz.meldung && <div role="status" className={ersatz.meldung.ok ? 'msg ok' : 'msg err'}>{ersatz.meldung.text}</div>}
|
||||||
{ersatz.html && (
|
{ersatz.html && (
|
||||||
<textarea
|
<textarea
|
||||||
readOnly
|
readOnly
|
||||||
className="form-control funktionen-panel-html"
|
className="textarea funktionen-panel-html"
|
||||||
rows={8}
|
rows={8}
|
||||||
value={ersatz.html}
|
value={ersatz.html}
|
||||||
aria-label="Ersatz-HTML zum Kopieren"
|
aria-label="Ersatz-HTML zum Kopieren"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Bereiche aus dem frueheren Admin-Panel (Gutschriften, Abo-Angebote, E-Mail-Speicher, Funktionen, Abo-Status).
|
/* Bereiche aus dem frueheren Admin-Panel (Gutschriften, Abo-Angebote, E-Mail-Speicher, Abo-Status).
|
||||||
Zeilen als Raster; unter 720 px untereinander, damit nichts waagerecht scrollt. */
|
Zeilen als Raster; unter 720 px untereinander, damit nichts waagerecht scrollt. */
|
||||||
.area-rows { display: flex; flex-direction: column; container-type: inline-size; }
|
.area-rows { display: flex; flex-direction: column; container-type: inline-size; }
|
||||||
.area-row {
|
.area-row {
|
||||||
@@ -21,34 +21,9 @@
|
|||||||
.area-filter { display: inline-flex; gap: 4px; margin-bottom: var(--sp-3); }
|
.area-filter { display: inline-flex; gap: 4px; margin-bottom: var(--sp-3); }
|
||||||
.area-inline { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
|
.area-inline { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
|
||||||
.storage-row { grid-template-columns: 150px 220px minmax(0, 1fr) 150px 110px; }
|
.storage-row { grid-template-columns: 150px 220px minmax(0, 1fr) 150px 110px; }
|
||||||
.funktionen-row { grid-template-columns: minmax(0, 1.2fr) 110px 200px 110px minmax(0, 1.4fr); }
|
|
||||||
.funktionen-sitekey { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; }
|
|
||||||
.funktionen-sitekey code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text); user-select: all; }
|
|
||||||
.funktionen-sitekey-copy {
|
|
||||||
display: inline-flex; align-items: center; justify-content: center; width: 25px; height: 25px; flex: 0 0 25px;
|
|
||||||
border: 0; border-radius: 4px; background: transparent; color: var(--text-faint); cursor: pointer;
|
|
||||||
}
|
|
||||||
.funktionen-sitekey-copy:hover { background: var(--surface-2); color: var(--text); }
|
|
||||||
.funktionen-sitekey-ok { color: var(--ok); }
|
|
||||||
.funktionen-panel { grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); background: var(--surface-2); }
|
|
||||||
.funktionen-panel-head { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); align-items: center; }
|
|
||||||
.funktionen-panel-warnungen { margin: 0; padding-left: 32px; }
|
|
||||||
.funktionen-panel .area-summary { margin-bottom: 0; }
|
|
||||||
.funktionen-panel-zeiten .area-error, .funktionen-panel-zeiten .area-message { margin: 0; }
|
|
||||||
.funktionen-panel-form { display: flex; flex-direction: column; gap: var(--sp-2); }
|
|
||||||
.funktionen-panel-feld { display: flex; flex-direction: column; gap: 4px; max-width: 520px; }
|
|
||||||
.funktionen-panel-html { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; min-height: 120px; }
|
|
||||||
.funktionen-panel-abschnitt { border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-1); min-width: 0; }
|
|
||||||
.funktionen-panel-abschnitt > summary { padding: 8px 12px; cursor: pointer; font-weight: 600; }
|
|
||||||
.funktionen-panel-abschnitt pre {
|
|
||||||
margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); max-width: 100%;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text);
|
|
||||||
white-space: pre-wrap; overflow-wrap: anywhere; user-select: text;
|
|
||||||
}
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); }
|
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); }
|
||||||
.area-row.head { display: none; }
|
.area-row.head { display: none; }
|
||||||
.funktionen-leer { display: none; }
|
|
||||||
.area-kv { grid-template-columns: minmax(0, 1fr); }
|
.area-kv { grid-template-columns: minmax(0, 1fr); }
|
||||||
.area-kv dt { margin-top: 6px; }
|
.area-kv dt { margin-top: 6px; }
|
||||||
}
|
}
|
||||||
@@ -72,5 +47,4 @@
|
|||||||
@container (max-width: 960px) {
|
@container (max-width: 960px) {
|
||||||
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); gap: 4px; }
|
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); gap: 4px; }
|
||||||
.area-row.head { display: none; }
|
.area-row.head { display: none; }
|
||||||
.funktionen-leer { display: none; }
|
|
||||||
}
|
}
|
||||||
@@ -1060,6 +1060,56 @@ button.link { font-weight: 500; } /* font: inherit setzt sonst das Gewicht von .
|
|||||||
.kunde-sub .badge { margin-right: 8px; vertical-align: top; }
|
.kunde-sub .badge { margin-right: 8px; vertical-align: top; }
|
||||||
/* Reiter "Abo & Angebote": Abstand zwischen Abo-Status und Angeboten (der Umschlag ist ein div, .page section + section greift dort nicht) */
|
/* Reiter "Abo & Angebote": Abstand zwischen Abo-Status und Angeboten (der Umschlag ist ein div, .page section + section greift dort nicht) */
|
||||||
.kunde-abo-angebote { margin-top: 36px; }
|
.kunde-abo-angebote { margin-top: 36px; }
|
||||||
|
/* Reiter "Funktionen": eine Tabelle, je Projekt eine Gruppenzeile; Bestaetigung und Panels sind Zusatzzeilen direkt unter der Funktionszeile */
|
||||||
|
.funktionen-group th { text-align: left; font-size: 14px; font-weight: 600; color: var(--text); padding-top: 20px; overflow-wrap: anywhere; white-space: normal; }
|
||||||
|
.funktionen-group .link { font-size: 13px; font-weight: 400; margin-left: 8px; }
|
||||||
|
.funktionen-sitekey { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 4px; }
|
||||||
|
.funktionen-sitekey code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; user-select: all; }
|
||||||
|
.funktionen-actions { display: flex; flex-wrap: wrap; gap: 4px 14px; }
|
||||||
|
.funktionen-confirm td { padding-top: 0; }
|
||||||
|
.funktionen-note { width: 100%; max-width: 260px; }
|
||||||
|
/* Zeilen einer Gruppe (Funktionszeile, Bestaetigung, Panels): nur die letzte behaelt die Haarlinie */
|
||||||
|
.funktionen-offen td { border-bottom: 0; }
|
||||||
|
.funktionen-extra td { padding-top: 0; }
|
||||||
|
.funktionen-panel { display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; background: var(--bg-subtle); border-radius: var(--radius); }
|
||||||
|
.funktionen-panel-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
|
||||||
|
.funktionen-panel-warnungen { margin: 0; padding-left: 18px; }
|
||||||
|
.funktionen-panel-abschnitt { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); min-width: 0; }
|
||||||
|
.funktionen-panel-abschnitt > summary { padding: 8px 12px; cursor: pointer; font-weight: 600; }
|
||||||
|
.funktionen-panel-abschnitt pre {
|
||||||
|
margin: 0; padding: 10px 12px; border-top: 1px solid var(--line); max-width: 100%;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text);
|
||||||
|
white-space: pre-wrap; overflow-wrap: anywhere; user-select: text;
|
||||||
|
}
|
||||||
|
/* Oeffnungszeiten live (Panel unter der Funktionszeile): Team-Formular und Ersatz-HTML */
|
||||||
|
.funktionen-panel-form { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.funktionen-panel-feld { display: flex; flex-direction: column; gap: 4px; max-width: 520px; }
|
||||||
|
.funktionen-panel-html { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; min-height: 120px; }
|
||||||
|
.funktionen-zustimmung { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
|
||||||
|
.funktionen-zustimmung input { flex: none; margin-top: 3px; width: 15px; height: 15px; accent-color: var(--accent); }
|
||||||
|
.funktionen-vorlage summary { cursor: pointer; }
|
||||||
|
.funktionen-vorlage p { margin: 6px 0; white-space: pre-wrap; }
|
||||||
|
.funktionen-msg { margin-top: 12px; }
|
||||||
|
/* Webshop-Anfrage je Projekt unter der Tabelle: ein Block je Eintrag, dazwischen eine Haarlinie */
|
||||||
|
.funktionen-shop { display: flex; flex-direction: column; gap: 8px; margin-top: 24px; }
|
||||||
|
.funktionen-shop + .funktionen-shop { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--line); }
|
||||||
|
.funktionen-shop dd { white-space: pre-wrap; }
|
||||||
|
.funktionen-begruendung { width: 100%; max-width: 560px; }
|
||||||
|
@media (min-width: 861px) {
|
||||||
|
.funktionen-shop .kv { grid-template-columns: max-content minmax(0, 1fr); }
|
||||||
|
}
|
||||||
|
/* Handy: Kopf nur fuer Screenreader, jede Funktionszeile als zwei Spalten (Name | Status, Ticket, Aktionen untereinander) */
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.funktionen-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||||
|
.funktionen-table, .funktionen-table tbody, .funktionen-table tr, .funktionen-table td, .funktionen-table th { display: block; }
|
||||||
|
.funktionen-table .funktionen-zeile { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 4px 12px; padding: 12px 0; border-bottom: 1px solid var(--line); }
|
||||||
|
.funktionen-table .funktionen-zeile.funktionen-offen { border-bottom: 0; }
|
||||||
|
.funktionen-zeile > td { grid-column: 2; min-width: 0; padding: 0; border-bottom: 0; }
|
||||||
|
.funktionen-zeile > td:first-child { grid-column: 1; grid-row: 1; }
|
||||||
|
.funktionen-zeile > td:empty, .funktionen-zeile > .funktionen-ohne-ticket { display: none; }
|
||||||
|
.funktionen-confirm td { padding-bottom: 12px; }
|
||||||
|
.funktionen-extra td { padding-bottom: 12px; }
|
||||||
|
}
|
||||||
/* ===== ENDE Kunden (P4) ===== */
|
/* ===== ENDE Kunden (P4) ===== */
|
||||||
|
|
||||||
/* ===== KOMPAT (bis P10) ===== */
|
/* ===== KOMPAT (bis P10) ===== */
|
||||||
|
|||||||
Reference in new issue
Block a user