Hosting-Produkte: Staffelpreise nach Laufzeit (1/3/6/12/24 Monate)
This commit is contained in:
parent
e0fd57532d
commit
5efe37ecc0
12 changed files with 145 additions and 28 deletions
|
|
@ -7,6 +7,7 @@ import { useSession } from '@/lib/session';
|
|||
import { Alert, eur, Field, fmt, INTERVAL, Status, CUSTOMER_ACTIONS, ACTION_LABEL } from '@/components/ui';
|
||||
import type { Prod } from '../page';
|
||||
|
||||
const TERMS = [1, 3, 6, 12, 24];
|
||||
type D = Prod & { versions: NonNullable<Prod['current']>[] };
|
||||
export default function Produkt() {
|
||||
const { id } = useParams<{ id: string }>(); const { can } = useSession(); const w = can('products.write');
|
||||
|
|
@ -18,8 +19,9 @@ export default function Produkt() {
|
|||
async function patch(b: object, ok: string) { try { await api('PATCH', `/admin/products/${id}`, b); setMsg({ k: 'ok', t: ok }); void load(); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } }
|
||||
async function newVersion(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault(); const f = new FormData(e.currentTarget); const once = cur.billingInterval === 'once';
|
||||
const termPrices = once ? [] : TERMS.map((m) => ({ termMonths: m, recurringCents: f.get(`term${m}`) === '' || f.get(`term${m}`) === null ? null : Math.round(Number(f.get(`term${m}`)) * 100) })).filter((t): t is { termMonths: number; recurringCents: number } => t.recurringCents !== null);
|
||||
try { await api('POST', `/admin/products/${id}/versions`, { name: f.get('name'), taxRateId: f.get('tax'), priceBasis: cur.priceBasis, setupCents: Math.round(Number(f.get('setup') || 0) * 100), recurringCents: once ? 0 : Math.round(Number(f.get('recurring') || 0) * 100), billingInterval: cur.billingInterval,
|
||||
termMonths: cur.termMonths, renewal: cur.renewal, renewalTermMonths: cur.renewalTermMonths || undefined, noticeDays: Number(f.get('notice') || cur.noticeDays), provisioning: d!.provisioning });
|
||||
termMonths: cur.termMonths, renewal: cur.renewal, renewalTermMonths: cur.renewalTermMonths || undefined, noticeDays: Number(f.get('notice') || cur.noticeDays), provisioning: d!.provisioning, termPrices });
|
||||
setOpen(false); setMsg({ k: 'ok', t: 'Neue Version angelegt. Bestehende Verträge behalten ihren Preis.' }); void load(); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); }
|
||||
}
|
||||
return (<>
|
||||
|
|
@ -29,6 +31,7 @@ export default function Produkt() {
|
|||
<div className="card"><h2>Aktuelle Konditionen (Version {cur.version})</h2>
|
||||
<p>{cur.setupCents > 0 && <>Einrichtung {eur(cur.setupCents)} {cur.priceBasis === 'gross' ? 'brutto' : 'netto'}<br /></>}{cur.billingInterval === 'once' ? 'Einmalpreis' : `${INTERVAL[cur.billingInterval]} ${eur(cur.recurringCents)} ${cur.priceBasis === 'gross' ? 'brutto' : 'netto'}`} · Steuer {cur.taxBp / 100} %</p>
|
||||
<p className="small muted">{cur.termMonths > 0 ? `Mindestlaufzeit ${cur.termMonths} Monate` : 'Keine Mindestlaufzeit'}{cur.renewal === 'auto' ? `, Verlängerung um ${cur.renewalTermMonths} Monat(e)` : ''}, Kündigungsfrist {cur.noticeDays} Tage · Bereitstellung: {d.connectorName ?? 'manuell'}{Object.keys(d.provisioning).length ? ` (${JSON.stringify(d.provisioning)})` : ''}</p>
|
||||
{d.termPrices.length > 0 && <p className="small muted">Laufzeit-Staffelpreise: {d.termPrices.map((t) => `${t.termMonths} Mon. ${eur(t.recurringCents)}`).join(' · ')}</p>}
|
||||
{w && <div className="row">{d.status !== 'active' && <button className="btn primary" onClick={() => patch({ status: 'active' }, 'Produkt aktiviert.')}>Aktivieren</button>}
|
||||
{d.status === 'active' && <button className="btn" onClick={() => patch({ status: 'retired' }, 'Produkt ausgelaufen. Es kann nicht mehr bestellt werden.')}>Auslaufen lassen</button>}
|
||||
<button className="btn" onClick={() => setOpen(!open)} aria-expanded={open}>Neue Version (Preis ändern)</button></div>}</div>
|
||||
|
|
@ -38,6 +41,11 @@ export default function Produkt() {
|
|||
{cur.billingInterval !== 'once' && <Field id="recurring" label={`Preis (€ ${cur.priceBasis === 'gross' ? 'brutto' : 'netto'})`}><input id="recurring" name="recurring" type="number" min={0} step="0.01" defaultValue={cur.recurringCents / 100} /></Field>}
|
||||
<Field id="tax" label="Steuersatz"><select id="tax" name="tax" defaultValue={tax.find((t) => t.rateBp === cur.taxBp)?.id}>{tax.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}</select></Field>
|
||||
<Field id="notice" label="Kündigungsfrist (Tage)"><input id="notice" name="notice" type="number" min={0} defaultValue={cur.noticeDays} /></Field></div>
|
||||
{cur.billingInterval !== 'once' && <>
|
||||
<h3>Staffelpreise nach Laufzeit (optional)</h3>
|
||||
<p className="small muted">Preis jeweils für die gesamte Laufzeit, nicht pro Monat. Leer lassen, wenn diese Laufzeit nicht angeboten wird.</p>
|
||||
<div className="cols">{TERMS.map((m) => <Field key={m} id={`term${m}`} label={`${m} Monat${m > 1 ? 'e' : ''} (€ ${cur.priceBasis === 'gross' ? 'brutto' : 'netto'}, gesamt)`}><input id={`term${m}`} name={`term${m}`} type="number" min={0} step="0.01" defaultValue={d.termPrices.find((t) => t.termMonths === m) ? d.termPrices.find((t) => t.termMonths === m)!.recurringCents / 100 : ''} /></Field>)}</div>
|
||||
</>}
|
||||
<div className="row"><button className="btn primary" type="submit">Version speichern</button><button className="btn" type="button" onClick={() => setOpen(false)}>Abbrechen</button></div></form>}
|
||||
<div className="card"><h2>Verkaufsregeln</h2><div className="row">
|
||||
<label className="row"><input type="checkbox" style={{ width: 20, minHeight: 20 }} disabled={!w} checked={d.orderableByCustomer} onChange={(e) => patch({ orderableByCustomer: e.target.checked }, 'Gespeichert.')} />Von Kunden selbst bestellbar</label>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Alert, Field, ACTION_LABEL, CUSTOMER_ACTIONS, INTERVAL } from '@/compon
|
|||
interface Conn { id: string; name: string; connector: string; capabilities: string[] }
|
||||
const LIMITS: [string, string, string][] = [['diskSpaceGb', 'Speicher (GB)', '10'], ['trafficGb', 'Traffic pro Monat (GB)', '100'], ['domains', 'Domains', '3'], ['subdomains', 'Subdomains', '10'], ['emailAccounts', 'Postfächer', '10'], ['emailAddresses', 'E-Mail-Adressen', '20'], ['emailForwardings', 'Weiterleitungen', '10'], ['databases', 'Datenbanken', '3'], ['ftpUsers', 'FTP-Benutzer', '3'], ['scheduledTasks', 'Cronjobs', '2']];
|
||||
const PERMS: [string, string][] = [['panel_access', 'Zugang zum Kundenpanel'], ['ftp', 'FTP'], ['php', 'PHP'], ['ssh', 'SSH'], ['backup', 'Backups'], ['file_manager', 'Dateimanager'], ['dns_editor', 'DNS-Editor'], ['certificate_management', 'SSL-Zertifikate verwalten'], ['domain_email', 'E-Mail-Domains'], ['email_catchall', 'Catch-all'], ['database_remote_access', 'Datenbank-Fernzugriff'], ['delete_main_domain', 'Hauptdomain löschen']];
|
||||
const TERMS = [1, 3, 6, 12, 24];
|
||||
|
||||
export default function HostingTarif() {
|
||||
const { can } = useSession();
|
||||
|
|
@ -21,11 +22,12 @@ export default function HostingTarif() {
|
|||
const limits = Object.fromEntries(LIMITS.map(([k]) => [k, unl[k] ? null : (g(k) === '' ? undefined : Number(g(k)))]).filter(([, v]) => v !== undefined));
|
||||
const permissions = Object.fromEntries(PERMS.map(([k]) => [k, f.get(`perm-${k}`) === 'on']));
|
||||
const once = interval === 'once';
|
||||
const termPrices = once ? [] : TERMS.map((m) => ({ termMonths: m, recurringCents: g(`term${m}`) === '' ? null : Math.round(Number(g(`term${m}`)) * 100) })).filter((t): t is { termMonths: number; recurringCents: number } => t.recurringCents !== null);
|
||||
try {
|
||||
const r = await api<{ productId: string; plan: { name: string; features: string[] } }>('POST', `/admin/connectors/${connId}/hosting-plans`, {
|
||||
plan: { name: g('planName'), limits, permissions },
|
||||
product: { sku: g('sku'), status: f.get('activate') === 'on' ? 'active' : 'draft', orderableByCustomer: f.get('orderable') === 'on', requiresApproval: f.get('approval') === 'on', customerActions: CUSTOMER_ACTIONS.filter((a) => f.get(`act-${a}`) === 'on'),
|
||||
version: { name: g('name') || g('planName'), description: g('desc') || undefined, taxRateId: g('tax'), priceBasis: basis, setupCents: Math.round(Number(g('setup') || 0) * 100), recurringCents: once ? 0 : Math.round(Number(g('recurring') || 0) * 100), billingInterval: interval, termMonths: once ? 0 : Number(g('term') || 0), renewal: !once && f.get('renewal') === 'on' ? 'auto' : 'none', noticeDays: Number(g('notice') || 30) } } });
|
||||
version: { name: g('name') || g('planName'), description: g('desc') || undefined, taxRateId: g('tax'), priceBasis: basis, setupCents: Math.round(Number(g('setup') || 0) * 100), recurringCents: once ? 0 : Math.round(Number(g('recurring') || 0) * 100), billingInterval: interval, termMonths: once ? 0 : Number(g('term') || 0), renewal: !once && f.get('renewal') === 'on' ? 'auto' : 'none', noticeDays: Number(g('notice') || 30), termPrices } } });
|
||||
setOk({ id: r.productId, name: r.plan.name, features: r.plan.features });
|
||||
} catch (x) { setErr(errMsg(x)); } finally { setBusy(false); }
|
||||
}
|
||||
|
|
@ -54,6 +56,11 @@ export default function HostingTarif() {
|
|||
<Field id="tax" label="Steuersatz"><select id="tax" name="tax" defaultValue={tax.find((t) => t.rateBp === 1900)?.id}>{tax.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}</select></Field>
|
||||
{interval !== 'once' && <><Field id="term" label="Mindestlaufzeit (Monate)"><input id="term" name="term" type="number" min={0} defaultValue={0} /></Field><Field id="notice" label="Kündigungsfrist (Tage)"><input id="notice" name="notice" type="number" min={0} defaultValue={30} /></Field></>}</div>
|
||||
{interval !== 'once' && <label className="row"><input type="checkbox" name="renewal" defaultChecked style={{ width: 20, minHeight: 20 }} />Automatische Verlängerung</label>}
|
||||
{interval !== 'once' && <>
|
||||
<h3>Staffelpreise nach Laufzeit (optional)</h3>
|
||||
<p className="small muted">Lässt Kunden bei der Bestellung eine Laufzeit wählen, z. B. günstiger bei 12 oder 24 Monaten. Preis jeweils für die gesamte Laufzeit, nicht pro Monat. Leer lassen, wenn diese Laufzeit nicht angeboten wird.</p>
|
||||
<div className="cols">{TERMS.map((m) => <Field key={m} id={`term${m}`} label={`${m} Monat${m > 1 ? 'e' : ''} (€ ${basis === 'gross' ? 'brutto' : 'netto'}, gesamt)`}><input id={`term${m}`} name={`term${m}`} type="number" min={0} step="0.01" /></Field>)}</div>
|
||||
</>}
|
||||
<h3>Verkauf</h3>
|
||||
<div className="row"><label className="row"><input type="checkbox" name="orderable" style={{ width: 20, minHeight: 20 }} />Von Kunden selbst bestellbar</label><label className="row"><input type="checkbox" name="approval" defaultChecked style={{ width: 20, minHeight: 20 }} />Manuelle Freigabe erforderlich</label><label className="row"><input type="checkbox" name="activate" style={{ width: 20, minHeight: 20 }} />Sofort aktivieren</label></div>
|
||||
<fieldset style={{ border: 0, padding: 0, margin: '12px 0' }}><legend className="small" style={{ fontWeight: 600, marginBottom: 8 }}>Was Kunden am Hosting-Konto selbst tun dürfen</legend><div className="row">{CUSTOMER_ACTIONS.map((a) => <label key={a} className="row small"><input type="checkbox" name={`act-${a}`} style={{ width: 20, minHeight: 20 }} defaultChecked={['panel.login', 'domain.manage', 'email.manage'].includes(a)} />{ACTION_LABEL[a]}</label>)}</div></fieldset>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { useSession } from '@/lib/session';
|
|||
import { Alert, Empty, eur, Field, INTERVAL, Status, CUSTOMER_ACTIONS, ACTION_LABEL } from '@/components/ui';
|
||||
|
||||
export interface Prod { id: string; sku: string; category: string; status: string; connectorInstanceId: string | null; connectorName: string | null; orderableByCustomer: boolean; requiresApproval: boolean; customerActions: string[]; provisioning: Record<string, unknown>;
|
||||
current: { id: string; version: number; name: string; taxBp: number; priceBasis: 'net' | 'gross'; setupCents: number; recurringCents: number; billingInterval: string; termMonths: number; renewal: string; renewalTermMonths: number; noticeDays: number } | null }
|
||||
current: { id: string; version: number; name: string; taxBp: number; priceBasis: 'net' | 'gross'; setupCents: number; recurringCents: number; billingInterval: string; termMonths: number; renewal: string; renewalTermMonths: number; noticeDays: number } | null;
|
||||
termPrices: { termMonths: number; recurringCents: number }[] }
|
||||
export const CAT: Record<string, string> = { hosting: 'Hosting', license: 'Lizenz', addon: 'Zusatzleistung', service: 'Dienstleistung' };
|
||||
const PSTATUS: Record<string, string> = { active: 'active', draft: 'invited', retired: 'closed' };
|
||||
export default function Produkte() {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,20 @@ import { useEffect, useState, type FormEvent } from 'react';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { Alert, Empty, Field, type Price, PriceText } from '@/components/ui';
|
||||
import { Alert, Empty, eur, Field, type Price, PriceText } from '@/components/ui';
|
||||
|
||||
interface P { id: string; sku: string; category: string; name: string; description: string | null; requiresApproval: boolean; price: Price; termMonths: number; renewal: string; renewalTermMonths: number; noticeDays: number }
|
||||
interface P { id: string; sku: string; category: string; name: string; description: string | null; requiresApproval: boolean; price: Price; termMonths: number; renewal: string; renewalTermMonths: number; noticeDays: number; termPrices: { termMonths: number; price: { net: number; tax: number; gross: number } }[] }
|
||||
interface Cust { id: string; name: string; customerNumber: string; customerType: 'private' | 'business' }
|
||||
export default function Bestellen() {
|
||||
const { me, can } = useSession(); const r = useRouter(); const staff = can('orders.write');
|
||||
const [custs, setCusts] = useState<Cust[]>([]); const [org, setOrg] = useState(''); const [cat, setCat] = useState<P[] | null>(null); const [sel, setSel] = useState<P | null>(null); const [err, setErr] = useState(''); const [busy, setBusy] = useState(false);
|
||||
const [custs, setCusts] = useState<Cust[]>([]); const [org, setOrg] = useState(''); const [cat, setCat] = useState<P[] | null>(null); const [sel, setSel] = useState<P | null>(null); const [term, setTerm] = useState<number | ''>(''); const [err, setErr] = useState(''); const [busy, setBusy] = useState(false);
|
||||
useEffect(() => { if (staff) api<Cust[]>('GET', '/admin/customers').then((l) => { setCusts(l); setOrg(l[0]?.id ?? ''); }).catch((e) => setErr(errMsg(e))); else if (me?.organizations[0]) setOrg(me.organizations[0].id); }, [staff, me]);
|
||||
useEffect(() => { if (org) api<P[]>('GET', `/catalog?org=${org}`).then(setCat).catch((e) => setErr(errMsg(e))); }, [org]);
|
||||
const priv = (cat?.[0] as (P & { customerType?: string }) | undefined)?.customerType === 'private';
|
||||
const selPrice: Price | null = sel && (term === '' ? sel.price : { ...sel.price, recurring: sel.termPrices.find((t) => t.termMonths === term)!.price });
|
||||
async function order(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault(); if (!sel) return; setBusy(true); setErr(''); const f = new FormData(e.currentTarget);
|
||||
try { const o = await api<{ status: string; number: string }>('POST', '/orders', { orgId: org, note: (f.get('note') as string) || undefined, items: [{ productId: sel.id, quantity: 1, ...(staff ? { discountBp: Math.round(Number(f.get('discount') || 0) * 100) } : {}) }] }); r.push(`/bestellungen?neu=${o.number}`); }
|
||||
try { const o = await api<{ status: string; number: string }>('POST', '/orders', { orgId: org, note: (f.get('note') as string) || undefined, items: [{ productId: sel.id, quantity: 1, ...(term !== '' ? { termMonths: term } : {}), ...(staff ? { discountBp: Math.round(Number(f.get('discount') || 0) * 100) } : {}) }] }); r.push(`/bestellungen?neu=${o.number}`); }
|
||||
catch (x) { setErr(errMsg(x)); } finally { setBusy(false); }
|
||||
}
|
||||
return (<>
|
||||
|
|
@ -25,8 +26,15 @@ export default function Bestellen() {
|
|||
<div className="grid">{cat.map((p) => <div className="card" key={p.id} style={sel?.id === p.id ? { borderColor: 'var(--accent)' } : undefined}><h2>{p.name}</h2>{p.description && <p className="muted">{p.description}</p>}
|
||||
<div className="small"><PriceText p={p.price} priv={priv} /></div>
|
||||
<p className="small muted">{p.termMonths > 0 ? `Mindestlaufzeit ${p.termMonths} Monate` : 'Keine Mindestlaufzeit'}{p.renewal === 'auto' ? `, Verlängerung um ${p.renewalTermMonths} Monat(e)` : ''}, Kündigungsfrist {p.noticeDays} Tage</p>
|
||||
<button className="btn" aria-pressed={sel?.id === p.id} onClick={() => setSel(p)}>{sel?.id === p.id ? 'Ausgewählt' : 'Auswählen'}</button></div>)}</div>
|
||||
{sel && <form className="card" onSubmit={order}><h2>Bestellung prüfen</h2><p><strong>{sel.name}</strong></p><div className="small" style={{ marginBottom: 16 }}><PriceText p={sel.price} priv={priv} /></div>
|
||||
<button className="btn" aria-pressed={sel?.id === p.id} onClick={() => { setSel(p); setTerm(''); }}>{sel?.id === p.id ? 'Ausgewählt' : 'Auswählen'}</button></div>)}</div>
|
||||
{sel && selPrice && <form className="card" onSubmit={order}><h2>Bestellung prüfen</h2><p><strong>{sel.name}</strong></p>
|
||||
{sel.termPrices.length > 0 && <Field id="term" label="Laufzeit" hint="Bei längerer Laufzeit oft günstiger."><select id="term" value={term} onChange={(e) => setTerm(e.target.value === '' ? '' : Number(e.target.value))}><option value="">Standard{sel.termMonths > 0 ? ` (${sel.termMonths} Monate)` : ''}</option>{sel.termPrices.map((t) => <option key={t.termMonths} value={t.termMonths}>{t.termMonths} Monate</option>)}</select></Field>}
|
||||
<div className="small" style={{ marginBottom: 16 }}>
|
||||
{term === '' ? <PriceText p={selPrice} priv={priv} /> : (<>
|
||||
{selPrice.setup.net > 0 && <div>Einrichtung: {priv ? `${eur(selPrice.setup.gross)} inkl. USt.` : `${eur(selPrice.setup.net)} netto (${eur(selPrice.setup.gross)} brutto)`}</div>}
|
||||
<div>Laufzeit {term} Monate, gesamt: {priv ? `${eur(selPrice.recurring.gross)} inkl. USt.` : `${eur(selPrice.recurring.net)} netto (${eur(selPrice.recurring.gross)} brutto)`}</div>
|
||||
</>)}
|
||||
</div>
|
||||
{staff && <Field id="discount" label="Rabatt in %" hint="Nur Personal. Der Preis wird serverseitig berechnet."><input id="discount" name="discount" type="number" min={0} max={100} step="0.01" defaultValue={0} /></Field>}
|
||||
<Field id="note" label="Anmerkung (optional)"><input id="note" name="note" maxLength={500} /></Field>
|
||||
<p className="small muted">{staff ? 'Die Bestellung wird sofort freigegeben und bereitgestellt.' : sel.requiresApproval ? 'Ihre Bestellung wird von uns geprüft und freigegeben. Sie sehen den Stand unter „Bestellungen“.' : 'Die Bereitstellung startet sofort.'}</p>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ type Row = { description: string; quantity: string; unitPrice: string; taxBp: st
|
|||
const emptyRow = (): Row => ({ description: '', quantity: '1', unitPrice: '', taxBp: '19' });
|
||||
interface Suggested { description: string; quantity: number; unitPriceNetCents: number; taxBp: number }
|
||||
const toRow = (it: Suggested): Row => ({ description: it.description, quantity: String(it.quantity), unitPrice: (it.unitPriceNetCents / 100).toFixed(2).replace('.', ','), taxBp: String(it.taxBp / 100) });
|
||||
interface Prod { id: string; status: string; current: { name: string } | null }
|
||||
interface Prod { id: string; status: string; current: { name: string } | null; termPrices: { termMonths: number }[] }
|
||||
interface Contract { id: string; number: string; productName: string | null }
|
||||
interface Dom { id: string; domain: string; sellNetCents: number | null }
|
||||
|
||||
|
|
@ -32,9 +32,9 @@ export default function RechnungDetail() {
|
|||
|
||||
const run = async (fn: () => Promise<unknown>, msg?: string) => { setErr(''); setOk(''); setBusy(true); try { await fn(); if (msg) setOk(msg); void load(); } catch (x) { setErr(errMsg(x)); } finally { setBusy(false); } };
|
||||
function appendSuggested(items: Suggested[]) { setRows((rs) => [...rs.filter((r) => r.description.trim() || r.unitPrice.trim()), ...items.map(toRow)]); }
|
||||
async function takeFrom(kind: 'product' | 'contract' | 'domain', pickId: string) {
|
||||
async function takeFrom(kind: 'product' | 'contract' | 'domain', pickId: string, termMonths?: string) {
|
||||
if (!pickId) return; setErr('');
|
||||
try { const r = await api<{ items: Suggested[] }>('GET', `/invoices/suggest-from-${kind}/${pickId}`); appendSuggested(r.items); } catch (x) { setErr(errMsg(x)); }
|
||||
try { const r = await api<{ items: Suggested[] }>('GET', `/invoices/suggest-from-${kind}/${pickId}${termMonths ? `?termMonths=${termMonths}` : ''}`); appendSuggested(r.items); } catch (x) { setErr(errMsg(x)); }
|
||||
}
|
||||
async function saveItems(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
|
@ -55,7 +55,11 @@ export default function RechnungDetail() {
|
|||
|
||||
{isDraft && inv.canWrite ? (<form className="card" onSubmit={saveItems}><h2>Positionen</h2>
|
||||
{(products.length > 0 || contracts.length > 0 || domains.length > 0) && <div className="cols" style={{ marginBottom: 16 }}>
|
||||
{products.length > 0 && <Field id="fromProduct" label="Position aus Produkt übernehmen" hint="Fügt Einrichtung und/oder wiederkehrenden Preis als eigene Zeilen hinzu."><select id="fromProduct" value="" onChange={(e) => void takeFrom('product', e.target.value)}><option value="">Produkt wählen …</option>{products.map((p) => <option key={p.id} value={p.id}>{p.current!.name}</option>)}</select></Field>}
|
||||
{products.length > 0 && <Field id="fromProduct" label="Position aus Produkt übernehmen" hint="Fügt Einrichtung und/oder wiederkehrenden Preis als eigene Zeilen hinzu. Bei Laufzeit-Staffelpreisen die gewünschte Laufzeit wählen."><select id="fromProduct" value="" onChange={(e) => { const [pid, tm] = e.target.value.split('|'); void takeFrom('product', pid!, tm); }}><option value="">Produkt wählen …</option>
|
||||
{products.map((p) => p.termPrices.length > 0
|
||||
? <optgroup key={p.id} label={p.current!.name}>{p.termPrices.map((t) => <option key={t.termMonths} value={`${p.id}|${t.termMonths}`}>{t.termMonths} Monate</option>)}</optgroup>
|
||||
: <option key={p.id} value={p.id}>{p.current!.name}</option>)}
|
||||
</select></Field>}
|
||||
{contracts.length > 0 && <Field id="fromContract" label="Position aus laufendem Vertrag übernehmen" hint="Übernimmt den wiederkehrenden Preis der aktuellen Periode."><select id="fromContract" value="" onChange={(e) => void takeFrom('contract', e.target.value)}><option value="">Vertrag wählen …</option>{contracts.map((c) => <option key={c.id} value={c.id}>{c.number} · {c.productName ?? '–'}</option>)}</select></Field>}
|
||||
{domains.length > 0 && <Field id="fromDomain" label="Position aus Domain übernehmen" hint="Verkaufspreis aus der Domain-Aufstellung, nie der Einkauf."><select id="fromDomain" value="" onChange={(e) => void takeFrom('domain', e.target.value)}><option value="">Domain wählen …</option>{domains.map((d) => <option key={d.id} value={d.id} disabled={d.sellNetCents === null}>{d.domain}{d.sellNetCents === null ? ' (kein Verkaufspreis)' : ''}</option>)}</select></Field>}
|
||||
</div>}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue