Directus - Neues Modul „Produktion": Board mit Neu/Warteschlange/Im Druck/ Kuvertierung/Versandfertig/Versendet/Archiv, zentraler Suche, Druck-Popup (Maschine, Template, Maße, Nummernbereich, Direktdruck vs. Warteschlange) und Empfängerliste mit Vorschau von Schriftstück und Kuvert. - Interface-Plugins: Auftragsdateien, Drucken, Testdruck, Empfängersuche. - Mailer-Hook für Auftrags- und Statusmails inkl. Signatur und AGB-Anhang. - Auftrag: Empfängerzeilen inline, Produktionsstatus (inkl. storniert), Versanddatum, Brief-/Kuvert-Indikatoren, Insights-Dashboard. - Absender aller Mails ist Skrift (Projektname + email_settings); die Angebotsanfrage nennt jetzt das angefragte Produkt. Preislogik - Eine gemeinsame Multiplikator-Formel; Normalpreis-Menge je Produkt (products.norm_qty). Alte B2B/B2C-Fallback-Felder entfernt. - Mengenrabatt-Staffel (ab X Stück Y %) in den Preis-Einstellungen. - Preis pro Stück ohne einmalige Kosten (unit_net_total). - Einzelversand rechnet Kuvert und Beschriftung nicht mehr doppelt. - Fehlerhafte Grundpreis-Formel legt die Preisauskunft nicht mehr lahm. - Briefnummern 1-basiert: Dateiname entspricht der Briefnummer. app.skrift.de - Direktlinks für Muster, Unterschriftenservice und Follow-ups; Muster als eigene Seite, Kontaktprodukte direkt im Anfrageformular. - Mehrere Adressen in den Stammdaten plus Adressauswahl im Konfigurator. - Verbotene Zeichen werden bei der Eingabe entfernt und gemeldet. - Zeilenüberlauf als Fehlerkasten vor „Weiter"; Mengenfeld leerbar, Standardmenge 100; Checkout-Checkboxen im Kartenstil. Produktion - skrift-agent: Poll-Modell je Maschine, Maß-Übersteuerung, Nummernfilter und automatischer Übergang in die Kuvertierung. WordPress - Neues Plugin „Skrift Preisrechner": Shortcodes inkl./exkl. MwSt, stündlicher Abgleich der Preisdaten in WordPress und ein sicherer Formel-Interpreter, damit Formeländerungen automatisch greifen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
5.8 KiB
JavaScript
150 lines
5.8 KiB
JavaScript
/**
|
||
* Skrift – Interface „Empfänger (durchsuchbar)"
|
||
* ---------------------------------------------------------------------------
|
||
* Zeigt im Auftrag alle Empfängerzeilen als kompakte, durchsuchbare Tabelle –
|
||
* die Produktionsansicht aus dem alten Job-Manager: pro Zeile die Briefnummer
|
||
* und die VOLLE Anschrift bzw. der Freitext (nicht nur der Name). Das Suchfeld
|
||
* filtert über die ganze Zeile (Name, Straße, Ort, Land, Platzhalter …).
|
||
*
|
||
* Rein lesend/suchend – bearbeitet wird eine Zeile weiterhin über das darunter
|
||
* liegende o2m-Feld „Empfänger (bearbeiten)".
|
||
*
|
||
* Bewusst von Hand als ESM geschrieben (kein Build-Schritt): Vue und der
|
||
* Extensions-SDK werden von Directus zur Laufzeit bereitgestellt.
|
||
*/
|
||
import { defineComponent, ref, computed, inject, watch, onMounted, h } from 'vue';
|
||
import { useApi } from '@directus/extensions-sdk';
|
||
|
||
const INLAND = /^(de|deutschland|germany)$/i;
|
||
|
||
/** Baut die volle Anzeigezeile einer Empfängerzeile. */
|
||
function volleZeile(e) {
|
||
if (e.free_text && String(e.free_text).trim()) {
|
||
return String(e.free_text).trim().split(/\r?\n/).map((s) => s.trim()).filter(Boolean).join(' · ');
|
||
}
|
||
const name = [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' ');
|
||
const strasse = [e.street, e.house_no].filter(Boolean).join(' ');
|
||
const ort = [e.zip, e.city].filter(Boolean).join(' ');
|
||
const land = e.country && !INLAND.test(String(e.country).trim()) ? e.country : null;
|
||
return [name, strasse, ort, land].filter(Boolean).join(' · ');
|
||
}
|
||
|
||
/** Zusätzliche freie Platzhalter als „Key: Wert"-Liste. */
|
||
function extras(e) {
|
||
if (!e.placeholders || typeof e.placeholders !== 'object') return '';
|
||
return Object.entries(e.placeholders)
|
||
.filter(([, v]) => v !== null && v !== undefined && String(v).trim() !== '')
|
||
.map(([k, v]) => `${k}: ${v}`).join(' · ');
|
||
}
|
||
|
||
const EmpfaengerSuche = defineComponent({
|
||
setup() {
|
||
const api = useApi();
|
||
const values = inject('values', ref({}));
|
||
const rows = ref([]);
|
||
const loading = ref(false);
|
||
const error = ref('');
|
||
const geladen = ref(false);
|
||
const suche = ref('');
|
||
|
||
const orderId = () => (values && values.value ? values.value.id : null);
|
||
|
||
async function laden() {
|
||
const id = orderId();
|
||
if (!id) { rows.value = []; geladen.value = true; return; }
|
||
loading.value = true; error.value = '';
|
||
try {
|
||
const res = await api.get('/items/order_entries', {
|
||
params: {
|
||
'filter[order][_eq]': id,
|
||
sort: 'letter_number',
|
||
limit: -1,
|
||
fields: '*',
|
||
},
|
||
});
|
||
const data = (res.data && res.data.data) || [];
|
||
rows.value = data.map((e) => ({
|
||
nr: e.letter_number,
|
||
zeile: volleZeile(e),
|
||
extra: extras(e),
|
||
}));
|
||
} catch (e) {
|
||
error.value = (e && e.response && e.response.data && e.response.data.errors &&
|
||
e.response.data.errors[0] && e.response.data.errors[0].message) ||
|
||
(e && e.message) || 'Empfänger konnten nicht geladen werden.';
|
||
} finally {
|
||
loading.value = false; geladen.value = true;
|
||
}
|
||
}
|
||
|
||
onMounted(laden);
|
||
watch(orderId, laden);
|
||
|
||
const treffer = computed(() => {
|
||
const q = suche.value.trim().toLowerCase();
|
||
if (!q) return rows.value;
|
||
return rows.value.filter((r) =>
|
||
(`#${r.nr} ${r.zeile} ${r.extra}`).toLowerCase().includes(q));
|
||
});
|
||
|
||
const hinweis = (txt) => h('div', { style: 'color:var(--foreground-subdued);font-size:14px;padding:8px 0;' }, txt);
|
||
|
||
return () => {
|
||
if (loading.value) return hinweis('Empfänger werden geladen …');
|
||
if (error.value) return hinweis(error.value);
|
||
if (!geladen.value) return hinweis('');
|
||
if (!rows.value.length) return hinweis('Noch keine Empfängerzeilen.');
|
||
|
||
const suchfeld = h('div', { style: 'display:flex;align-items:center;gap:10px;margin-bottom:10px;' }, [
|
||
h('input', {
|
||
type: 'search',
|
||
value: suche.value,
|
||
placeholder: 'Empfänger durchsuchen (Name, Straße, Ort, Platzhalter …)',
|
||
onInput: (ev) => { suche.value = ev.target.value; },
|
||
style: 'flex:1;padding:8px 12px;border:1px solid var(--border-normal);border-radius:6px;' +
|
||
'background:var(--background-page);color:var(--foreground-normal);font:inherit;font-size:14px;',
|
||
}),
|
||
h('span', { style: 'font-size:12px;color:var(--foreground-subdued);white-space:nowrap;' },
|
||
`${treffer.value.length} von ${rows.value.length}`),
|
||
]);
|
||
|
||
const zeilen = treffer.value.map((r) =>
|
||
h('div', {
|
||
key: r.nr,
|
||
style: 'display:flex;align-items:baseline;gap:12px;padding:7px 12px;' +
|
||
'border:1px solid var(--border-subdued);border-radius:6px;',
|
||
}, [
|
||
h('span', {
|
||
style: 'min-width:44px;font-family:var(--family-monospace, monospace);font-size:13px;' +
|
||
'font-weight:600;color:var(--primary);',
|
||
}, `#${r.nr}`),
|
||
h('div', { style: 'flex:1;font-size:14px;line-height:1.4;word-break:break-word;' }, [
|
||
h('span', {}, r.zeile || '—'),
|
||
r.extra
|
||
? h('span', { style: 'display:block;font-size:12px;color:var(--foreground-subdued);margin-top:2px;' }, r.extra)
|
||
: null,
|
||
]),
|
||
]),
|
||
);
|
||
|
||
const body = treffer.value.length
|
||
? h('div', { style: 'display:flex;flex-direction:column;gap:5px;' }, zeilen)
|
||
: hinweis('Keine Treffer.');
|
||
|
||
return h('div', {}, [suchfeld, body]);
|
||
};
|
||
},
|
||
});
|
||
|
||
export default {
|
||
id: 'skrift-order-entries',
|
||
name: 'Empfänger (durchsuchbar)',
|
||
icon: 'search',
|
||
description: 'Durchsuchbare Empfängertabelle: Briefnummer + volle Anschrift (Produktionsansicht).',
|
||
component: EmpfaengerSuche,
|
||
types: ['alias'],
|
||
localTypes: ['presentation'],
|
||
group: 'presentation',
|
||
options: null,
|
||
};
|