Phase2: Invoicecomputation/AdminpricingUI/Ainvoicemgnt
Some checks failed
Build and Push / build (push) Failing after 45s

This commit is contained in:
2026-05-24 14:12:26 +02:00
parent cdc2210eaf
commit d4fcc33bc1
4 changed files with 26 additions and 27 deletions

View File

@@ -26,9 +26,16 @@ export async function GET(
if (!pdf) { if (!pdf) {
return new NextResponse("Not found", { status: 404 }); return new NextResponse("Not found", { status: 404 });
} }
// Construct a response that the browser will render inline (PDF // Web `Response`'s `BodyInit` doesn't include Node's `Buffer` — pg
// viewer) but also offer to download with the right filename. // returns bytea as Buffer but Next/the runtime want a BufferSource.
return new NextResponse(pdf.data, { // Wrap into a zero-copy Uint8Array view (Buffer extends Uint8Array
// under the hood, but TypeScript treats them as distinct).
const body = new Uint8Array(
pdf.data.buffer,
pdf.data.byteOffset,
pdf.data.byteLength
);
return new NextResponse(body, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/pdf", "Content-Type": "application/pdf",

View File

@@ -301,7 +301,7 @@ function StatusPill({ status }: { status: InvoiceStatus }) {
<span <span
className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${color}`} className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${color}`}
> >
{t(`status_${status}` as any)} {t(`status_${status}`)}
</span> </span>
); );
} }

View File

@@ -73,7 +73,7 @@ export function InvoicesTable({ initialInvoices }: Props) {
> >
{STATUS_FILTERS.map((s) => ( {STATUS_FILTERS.map((s) => (
<option key={s} value={s}> <option key={s} value={s}>
{s === "all" ? t("allStatuses") : t(`status_${s}` as any)} {s === "all" ? t("allStatuses") : t(`status_${s}`)}
</option> </option>
))} ))}
</select> </select>
@@ -177,7 +177,7 @@ function StatusPill({ status }: { status: InvoiceStatus }) {
<span <span
className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${color}`} className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${color}`}
> >
{t(`status_${status}` as any)} {t(`status_${status}`)}
</span> </span>
); );
} }

View File

@@ -88,22 +88,17 @@ export function PricingEditor({
const [addingSkill, setAddingSkill] = useState(false); const [addingSkill, setAddingSkill] = useState(false);
const [skillError, setSkillError] = useState(""); const [skillError, setSkillError] = useState("");
const addOrUpdateSkill = async ( // Core upsert — used by both the "add new skill" form and the inline
e: React.FormEvent, // editor on existing rows. Kept event-free so callers can invoke it
overrideId?: string, // without synthesizing a fake form event.
overridePrice?: string const upsertSkillPrice = async (skillId: string, dailyPriceChf: number) => {
) => {
e.preventDefault();
setAddingSkill(true); setAddingSkill(true);
setSkillError(""); setSkillError("");
try { try {
const res = await fetch("/api/admin/billing/skill-pricing", { const res = await fetch("/api/admin/billing/skill-pricing", {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({ skillId, dailyPriceChf }),
skillId: overrideId ?? newSkillId,
dailyPriceChf: Number(overridePrice ?? newSkillPrice),
}),
}); });
if (!res.ok) { if (!res.ok) {
const j = await res.json().catch(() => ({})); const j = await res.json().catch(() => ({}));
@@ -117,6 +112,12 @@ export function PricingEditor({
} }
}; };
const onAddNewSkill = (e: React.FormEvent) => {
e.preventDefault();
if (!newSkillId) return;
void upsertSkillPrice(newSkillId, Number(newSkillPrice));
};
const deleteSkill = async (skillId: string) => { const deleteSkill = async (skillId: string) => {
if (!confirm(t("confirmDeleteSkillPrice", { skill: skillId }))) return; if (!confirm(t("confirmDeleteSkillPrice", { skill: skillId }))) return;
setSkillError(""); setSkillError("");
@@ -254,13 +255,7 @@ export function PricingEditor({
<InlinePriceEditor <InlinePriceEditor
skillId={sp.skillId} skillId={sp.skillId}
initialPrice={sp.dailyPriceChf} initialPrice={sp.dailyPriceChf}
onSave={(price) => onSave={(price) => upsertSkillPrice(sp.skillId, price)}
addOrUpdateSkill(
new Event("submit") as any,
sp.skillId,
String(price)
)
}
/> />
</td> </td>
<td className="py-2 text-right"> <td className="py-2 text-right">
@@ -280,10 +275,7 @@ export function PricingEditor({
<p className="text-sm text-text-muted italic mb-4">{t("noSkillsPriced")}</p> <p className="text-sm text-text-muted italic mb-4">{t("noSkillsPriced")}</p>
)} )}
<form <form onSubmit={onAddNewSkill} className="flex items-end gap-3">
onSubmit={(e) => addOrUpdateSkill(e)}
className="flex items-end gap-3"
>
<label className="flex-grow"> <label className="flex-grow">
<span className="text-xs text-text-muted">{t("addSkillLabel")}</span> <span className="text-xs text-text-muted">{t("addSkillLabel")}</span>
<select <select