Tabulator-Support in Brieftexten + ausstehende Batches
Tabs (Eingabe + Rendering): - Tab-Taste fügt echten Tabulator ein (App-Konfigurator + Directus manueller Druck); \t in beiden Zeichen-Whitelists erlaubt. - Renderer: Zeilen mit führenden Tabs werden nicht mehr nach Wortbreite umgebrochen; Tab-Spruenge an den rechten Rand geclampt; bei Ueberlauf rechtsbuendig (Datum ganz rechts, ohne Absturz nach links). Tab-loser Text bleibt bit-identisch. Mailer: - loadSettings bricht nicht mehr ab, wenn team_email fehlt (Team- Empfaenger separat gelesen). - Eingebaute Fallback-Vorlagen fuer order_created + Status-Mails, damit Bestaetigung/Status auch ohne DB-Vorlage zugestellt werden. Weitere ausstehende Arbeit: - Abweichender Text je Schriftstueck (#5) in Generierung/Konfigurator. - Produktions-Board: Format-Sammelgruppen (Bulk) + Bootstrap-Feld. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,8 @@ type Entry = {
|
||||
/** Alternative zur strukturierten Adresse. */
|
||||
free_text: string;
|
||||
placeholders: Record<string, string>;
|
||||
/** Abweichender Text NUR für dieses Schriftstück (leer/undefined = Text aus #1). */
|
||||
text?: string;
|
||||
};
|
||||
|
||||
type Adresse = {
|
||||
@@ -319,6 +321,8 @@ export default function Configurator({ vorwahlProdukt }: { vorwahlProdukt?: stri
|
||||
const [textservice, setTextservice] = useState(entwurf.textservice ?? false);
|
||||
const [textBriefing, setTextBriefing] = useState(entwurf.textBriefing ?? '');
|
||||
const [textTemplate, setTextTemplate] = useState(entwurf.textTemplate ?? '');
|
||||
/** Aktuell im Textfeld angezeigtes Schriftstück (0 = Standardtext, der für alle gilt). */
|
||||
const [textDocIndex, setTextDocIndex] = useState(0);
|
||||
|
||||
const [rechnung, setRechnung] = useState<Adresse>(entwurf.rechnung ?? leereAdresse());
|
||||
const [lieferAbweichend, setLieferAbweichend] = useState(entwurf.lieferAbweichend ?? false);
|
||||
@@ -359,8 +363,9 @@ export default function Configurator({ vorwahlProdukt }: { vorwahlProdukt?: stri
|
||||
const wl = settings?.char_whitelist;
|
||||
if (!wl) return null;
|
||||
// Zeilenumbrüche sind im mehrzeiligen Text erlaubt; einzeilige Felder
|
||||
// entfernen \n/\r ohnehin separat vor saeubere().
|
||||
try { return new RegExp(`[^${wl}\\n\\r]`, 'g'); } catch { return null; }
|
||||
// entfernen \n/\r ohnehin separat vor saeubere(). Tabs (\t) sind erlaubt,
|
||||
// damit man z.B. das Datum an den rechten Rand rücken kann.
|
||||
try { return new RegExp(`[^${wl}\\n\\r\\t]`, 'g'); } catch { return null; }
|
||||
}, [settings?.char_whitelist]);
|
||||
/** Entfernt verbotene Zeichen und meldet sie; gibt den bereinigten Wert zurück. */
|
||||
const saeubere = (v: string) => {
|
||||
@@ -530,12 +535,30 @@ export default function Configurator({ vorwahlProdukt }: { vorwahlProdukt?: stri
|
||||
|
||||
/** Zeichengrenze richtet sich nach dem Format – wie bisher. */
|
||||
const maxText = format?.startsWith('a4') ? MAX_TEXT.a4 : MAX_TEXT.a6;
|
||||
const textZuLang = textTemplate.length > maxText;
|
||||
|
||||
// ── Text je Schriftstück (#5) ──────────────────────────────────────────────
|
||||
// Pfeile im Textfeld: pro Dokument ein eigener Text möglich (Standard = #1).
|
||||
// Nur im Tabellenmodus (≤ 25) und bei echtem Text (kein Textservice/Datei).
|
||||
const proDokText = !textservice && !dateiModus && !nurUpload && entries.length > 1;
|
||||
const textDocs = Math.min(entries.length, TABELLEN_GRENZE);
|
||||
// Angezeigter Text: #1 = Standardtext (textTemplate); ab #2 der Override bzw.
|
||||
// – solange nicht geändert – der Standardtext.
|
||||
const aktDocText = textDocIndex === 0 ? textTemplate : (entries[textDocIndex]?.text ?? textTemplate);
|
||||
const setDocText = (v: string) => {
|
||||
const clean = saeubere(v);
|
||||
if (textDocIndex === 0) { setTextTemplate(clean); return; }
|
||||
setEntries((prev) => prev.map((e, i) => (i === textDocIndex ? { ...e, text: clean } : e)));
|
||||
setPreviewSession(null);
|
||||
};
|
||||
const textZuLang = aktDocText.length > maxText;
|
||||
|
||||
// Ändert sich Text, Format oder Schrift, ist ein früher erkannter Überlauf
|
||||
// hinfällig – erst die nächste Vorschau entscheidet erneut.
|
||||
useEffect(() => { setTextUeberlauf(false); }, [textTemplate, format, font]);
|
||||
|
||||
// Dokument-Index nicht über die Anzahl hinaus stehen lassen.
|
||||
useEffect(() => { if (textDocIndex >= textDocs) setTextDocIndex(0); }, [textDocIndex, textDocs]);
|
||||
|
||||
// Ändert sich irgendetwas, das die gerenderten Dokumente beeinflusst, wird die
|
||||
// Vorschau-Session ungültig – sonst würde bei der Bestellung veralteter Inhalt
|
||||
// 1:1 übernommen. Nur eine unveränderte Vorschau wird wiederverwendet.
|
||||
@@ -1053,6 +1076,8 @@ export default function Configurator({ vorwahlProdukt }: { vorwahlProdukt?: stri
|
||||
country: e.country,
|
||||
free_text: adressModus === 'freitext' ? e.free_text : '',
|
||||
placeholders: e.placeholders,
|
||||
// Abweichender Text nur, wenn wirklich gesetzt (sonst greift der Standardtext).
|
||||
text: (e.text && e.text.trim()) ? e.text : undefined,
|
||||
})),
|
||||
});
|
||||
// Auftrag ist abgesendet – der zwischengespeicherte Entwurf wird verworfen.
|
||||
@@ -1626,16 +1651,49 @@ export default function Configurator({ vorwahlProdukt }: { vorwahlProdukt?: stri
|
||||
</div>
|
||||
) : (
|
||||
<div className="sk-field" style={{ marginBottom: 0 }}>
|
||||
<label className="sk-label" htmlFor="text">
|
||||
Inhalt des Schriftstücks <span className="sk-note">· max. {maxText} Zeichen</span>
|
||||
</label>
|
||||
<textarea id="text" className="sk-input" rows={10} value={textTemplate}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 'var(--sk-space-2)', marginBottom: 'var(--sk-space-2)' }}>
|
||||
<label className="sk-label" htmlFor="text" style={{ marginBottom: 0 }}>
|
||||
Inhalt des Schriftstücks <span className="sk-note">· max. {maxText} Zeichen</span>
|
||||
</label>
|
||||
{/* Pfeile: pro Schriftstück eigenen Text eingeben (Standard = #1). */}
|
||||
{proDokText && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<button type="button" className="sk-pager-btn" aria-label="Vorheriges Schriftstück"
|
||||
disabled={textDocIndex === 0}
|
||||
onClick={() => setTextDocIndex((i) => Math.max(0, i - 1))}>‹</button>
|
||||
<span className="sk-note" style={{ whiteSpace: 'nowrap' }}>
|
||||
Schriftstück {textDocIndex + 1}/{textDocs}
|
||||
</span>
|
||||
<button type="button" className="sk-pager-btn" aria-label="Nächstes Schriftstück"
|
||||
disabled={textDocIndex >= textDocs - 1}
|
||||
onClick={() => setTextDocIndex((i) => Math.min(textDocs - 1, i + 1))}>›</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<textarea id="text" className="sk-input" rows={10} value={aktDocText}
|
||||
maxLength={maxText}
|
||||
onChange={(e) => setTextTemplate(saeubere(e.target.value))} />
|
||||
onChange={(e) => setDocText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
// Tab-Taste fügt einen echten Tabulator ein (statt den Fokus
|
||||
// zu verschieben) – für z.B. das Datum am rechten Rand.
|
||||
if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
e.preventDefault();
|
||||
if (aktDocText.length >= maxText) return;
|
||||
const ta = e.currentTarget;
|
||||
const s = ta.selectionStart ?? aktDocText.length;
|
||||
const en = ta.selectionEnd ?? s;
|
||||
setDocText(aktDocText.slice(0, s) + '\t' + aktDocText.slice(en));
|
||||
requestAnimationFrame(() => { ta.selectionStart = ta.selectionEnd = s + 1; });
|
||||
}} />
|
||||
<p className={textZuLang ? 'sk-error' : 'sk-note'} style={{ marginTop: 'var(--sk-space-2)' }}>
|
||||
{textTemplate.length}/{maxText} Zeichen
|
||||
{aktDocText.length}/{maxText} Zeichen
|
||||
{textZuLang && ' – Text ist zu lang für das gewählte Format.'}
|
||||
</p>
|
||||
{proDokText && textDocIndex > 0 && (
|
||||
<p className="sk-error" style={{ marginTop: 'var(--sk-space-2)' }}>
|
||||
Dieser Text wird nur für Schriftstück {textDocIndex + 1} verwendet (Standard = Text aus #1).
|
||||
</p>
|
||||
)}
|
||||
<p className="sk-note" style={{ marginTop: '8px' }}>
|
||||
{standardPlatzhalter.length > 0 ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user