Show staff-only products in the order catalog for staff
POST /orders already lets staff order products that customers cannot order themselves, but the catalog hid them, so e.g. KeyHelp Unlimited could not be booked for a customer. Staff now see all active products, marked as staff-only where applicable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
466bd83b7e
commit
c276eb8ceb
2 changed files with 7 additions and 5 deletions
|
|
@ -11,7 +11,7 @@ import { one, query, run, tx } from '../../core/db.js';
|
||||||
import { audit } from '../../core/audit.js';
|
import { audit } from '../../core/audit.js';
|
||||||
import { clientIp, requireAuth, requirePermission } from '../../core/auth.js';
|
import { clientIp, requireAuth, requirePermission } from '../../core/auth.js';
|
||||||
import { AppError, badRequest, conflict, notFound } from '../../core/errors.js';
|
import { AppError, badRequest, conflict, notFound } from '../../core/errors.js';
|
||||||
import { canInOrg } from '../../core/policy.js';
|
import { can, canInOrg } from '../../core/policy.js';
|
||||||
import type { KcModule } from '../../core/module.js';
|
import type { KcModule } from '../../core/module.js';
|
||||||
|
|
||||||
const int = (max: number) => z.number().int().min(0).max(max);
|
const int = (max: number) => z.number().int().min(0).max(max);
|
||||||
|
|
@ -272,7 +272,9 @@ export const catalogModule: KcModule = {
|
||||||
const q = z.object({ org: z.string().uuid() }).parse(req.query);
|
const q = z.object({ org: z.string().uuid() }).parse(req.query);
|
||||||
if (!canInOrg(a.principal, q.org, 'orders.read', 'products.read')) throw notFound();
|
if (!canInOrg(a.principal, q.org, 'orders.read', 'products.read')) throw notFound();
|
||||||
const org = await one('SELECT customer_type FROM organizations WHERE id = ?', [q.org]);
|
const org = await one('SELECT customer_type FROM organizations WHERE id = ?', [q.org]);
|
||||||
const rows = await query(`${productSelect} WHERE p.status = 'active' AND p.orderable_by_customer = 1 ORDER BY v.name`);
|
// Personal darf auch Produkte bestellen, die Kunden nicht selbst bestellen können (siehe POST /orders)
|
||||||
|
const staff = can(a.principal, 'orders.write');
|
||||||
|
const rows = await query(`${productSelect} WHERE p.status = 'active'${staff ? '' : ' AND p.orderable_by_customer = 1'} ORDER BY v.name`);
|
||||||
return Promise.all(rows.map(async (r) => {
|
return Promise.all(rows.map(async (r) => {
|
||||||
const price = calculatePrice({ basis: r.price_basis, setupCents: r.setup_cents, recurringCents: r.recurring_cents, taxBp: r.tax_bp, interval: r.billing_interval, quantity: 1, discountBp: 0 });
|
const price = calculatePrice({ basis: r.price_basis, setupCents: r.setup_cents, recurringCents: r.recurring_cents, taxBp: r.tax_bp, interval: r.billing_interval, quantity: 1, discountBp: 0 });
|
||||||
// Staffelpreise nach Laufzeit (optional): je gewählter Laufzeit ein eigener Gesamtpreis statt des Basispreises.
|
// Staffelpreise nach Laufzeit (optional): je gewählter Laufzeit ein eigener Gesamtpreis statt des Basispreises.
|
||||||
|
|
@ -280,7 +282,7 @@ export const catalogModule: KcModule = {
|
||||||
termMonths: t.term_months,
|
termMonths: t.term_months,
|
||||||
price: calculatePrice({ basis: r.price_basis, setupCents: 0, recurringCents: t.recurring_cents, taxBp: r.tax_bp, interval: r.billing_interval, quantity: 1, discountBp: 0 }).recurring,
|
price: calculatePrice({ basis: r.price_basis, setupCents: 0, recurringCents: t.recurring_cents, taxBp: r.tax_bp, interval: r.billing_interval, quantity: 1, discountBp: 0 }).recurring,
|
||||||
}));
|
}));
|
||||||
return { id: r.id, sku: r.sku, category: r.category, name: r.vname, description: r.description, requiresApproval: !!r.requires_approval, customerType: org?.customer_type, price, termMonths: r.term_months, renewal: r.renewal, renewalTermMonths: r.renewal_term_months, noticeDays: r.notice_days, termPrices: tiers };
|
return { id: r.id, sku: r.sku, category: r.category, name: r.vname, description: r.description, requiresApproval: !!r.requires_approval, orderableByCustomer: !!r.orderable_by_customer, customerType: org?.customer_type, price, termMonths: r.term_months, renewal: r.renewal, renewalTermMonths: r.renewal_term_months, noticeDays: r.notice_days, termPrices: tiers };
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { api, errMsg } from '@/lib/api';
|
||||||
import { useSession } from '@/lib/session';
|
import { useSession } from '@/lib/session';
|
||||||
import { Alert, Empty, eur, 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; termPrices: { termMonths: number; price: { net: number; tax: number; gross: number } }[] }
|
interface P { id: string; sku: string; category: string; name: string; description: string | null; requiresApproval: boolean; orderableByCustomer?: 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' }
|
interface Cust { id: string; name: string; customerNumber: string; customerType: 'private' | 'business' }
|
||||||
export default function Bestellen() {
|
export default function Bestellen() {
|
||||||
const { me, can } = useSession(); const r = useRouter(); const staff = can('orders.write');
|
const { me, can } = useSession(); const r = useRouter(); const staff = can('orders.write');
|
||||||
|
|
@ -23,7 +23,7 @@ export default function Bestellen() {
|
||||||
<h1>Neue Bestellung</h1>{err && <Alert kind="err">{err}</Alert>}
|
<h1>Neue Bestellung</h1>{err && <Alert kind="err">{err}</Alert>}
|
||||||
{staff && <div className="card"><Field id="org" label="Kunde"><select id="org" value={org} onChange={(e) => { setOrg(e.target.value); setSel(null); }}>{custs.map((c) => <option key={c.id} value={c.id}>{c.customerNumber} · {c.name} ({c.customerType === 'private' ? 'Privat' : 'Geschäft'})</option>)}</select></Field></div>}
|
{staff && <div className="card"><Field id="org" label="Kunde"><select id="org" value={org} onChange={(e) => { setOrg(e.target.value); setSel(null); }}>{custs.map((c) => <option key={c.id} value={c.id}>{c.customerNumber} · {c.name} ({c.customerType === 'private' ? 'Privat' : 'Geschäft'})</option>)}</select></Field></div>}
|
||||||
{cat === null ? <p className="muted" role="status">Wird geladen …</p> : cat.length === 0 ? <div className="card"><Empty title="Keine Produkte bestellbar">Aktuell sind keine Produkte für die Selbstbestellung freigegeben.</Empty></div> : (<>
|
{cat === null ? <p className="muted" role="status">Wird geladen …</p> : cat.length === 0 ? <div className="card"><Empty title="Keine Produkte bestellbar">Aktuell sind keine Produkte für die Selbstbestellung freigegeben.</Empty></div> : (<>
|
||||||
<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="grid">{cat.map((p) => <div className="card" key={p.id} style={sel?.id === p.id ? { borderColor: 'var(--accent)' } : undefined}><h2>{p.name}</h2>{staff && p.orderableByCustomer === false && <p className="small muted">Nur durch das Personal bestellbar</p>}{p.description && <p className="muted">{p.description}</p>}
|
||||||
<div className="small"><PriceText p={p.price} priv={priv} /></div>
|
<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>
|
<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); setTerm(''); }}>{sel?.id === p.id ? 'Ausgewählt' : 'Auswählen'}</button></div>)}</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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue