feat(admin): CSV-Export der Kunden für die Buchhaltung
GET /admin/export/accounts.csv — eine Zeile pro Kunde (Profil + aktuelles Abo + Plan-Preis + Instanzen). Semikolon-getrennt, UTF-8 BOM (Excel-CH), Content-Disposition mit Datum. Nur mit Admin-Token (401 sonst). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,45 @@ adminRouter.get("/accounts/:id", async (req, res) => {
|
|||||||
res.json({ account, subscriptions, instances });
|
res.json({ account, subscriptions, instances });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// — CSV-Export für die Buchhaltung —
|
||||||
|
// Eine Zeile pro Kunde mit aktuellem Abo + Plan-Preis. Semikolon-getrennt
|
||||||
|
// (Excel-CH-freundlich), UTF-8 BOM, damit Umlaute korrekt erscheinen.
|
||||||
|
function csvCell(v) {
|
||||||
|
const s = v == null ? "" : String(v);
|
||||||
|
return /[";\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
||||||
|
}
|
||||||
|
adminRouter.get("/export/accounts.csv", async (_req, res) => {
|
||||||
|
const { rows } = await query(`
|
||||||
|
select a.email, a.company, a.contact_name, a.street, a.zip, a.city, a.country, a.created_at,
|
||||||
|
s.plan, s.status as sub_status, s.current_period_end,
|
||||||
|
coalesce(i.cnt, 0)::int as instance_count
|
||||||
|
from accounts a
|
||||||
|
left join lateral (
|
||||||
|
select plan, status, current_period_end from subscriptions
|
||||||
|
where account_id = a.id order by created_at desc limit 1
|
||||||
|
) s on true
|
||||||
|
left join (select account_id, count(*) cnt from instances group by account_id) i
|
||||||
|
on i.account_id = a.id
|
||||||
|
order by a.created_at
|
||||||
|
`);
|
||||||
|
const header = ["Email","Firma","Ansprechperson","Strasse","PLZ","Ort","Land",
|
||||||
|
"Kunde_seit","Plan","Abo_Status","Periode_bis","Preis_CHF_mtl","Instanzen"];
|
||||||
|
const lines = [header.join(";")];
|
||||||
|
for (const r of rows) {
|
||||||
|
const price = getPlan(r.plan)?.priceChf ?? "";
|
||||||
|
lines.push([
|
||||||
|
r.email, r.company, r.contact_name, r.street, r.zip, r.city, r.country,
|
||||||
|
r.created_at ? new Date(r.created_at).toISOString().slice(0, 10) : "",
|
||||||
|
r.plan || "", r.sub_status || "", r.current_period_end ? new Date(r.current_period_end).toISOString().slice(0, 10) : "",
|
||||||
|
r.sub_status === "active" ? price : "", r.instance_count,
|
||||||
|
].map(csvCell).join(";"));
|
||||||
|
}
|
||||||
|
const csv = "" + lines.join("\r\n");
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="rapport-kunden-${new Date().toISOString().slice(0,10)}.csv"`);
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
// — Instanzen eines Kontos (Kurzform) —
|
// — Instanzen eines Kontos (Kurzform) —
|
||||||
adminRouter.get("/accounts/:id/instances", async (req, res) => {
|
adminRouter.get("/accounts/:id/instances", async (req, res) => {
|
||||||
const { rows } = await query(
|
const { rows } = await query(
|
||||||
|
|||||||
Reference in New Issue
Block a user