feat(customers): add initial admin bootstrap and csv import
This commit is contained in:
parent
c816e9869d
commit
ecab0fe6b6
27 changed files with 1197 additions and 34 deletions
14
frontend/athena/app/api/customers/import/commit/route.ts
Normal file
14
frontend/athena/app/api/customers/import/commit/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { assertSameOrigin } from "@/lib/server/request-guards";
|
||||
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const originError = assertSameOrigin(request);
|
||||
|
||||
if (originError) {
|
||||
return originError;
|
||||
}
|
||||
|
||||
return proxyHermesRequest(request, "/customers/import/commit");
|
||||
}
|
||||
14
frontend/athena/app/api/customers/import/preview/route.ts
Normal file
14
frontend/athena/app/api/customers/import/preview/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { assertSameOrigin } from "@/lib/server/request-guards";
|
||||
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const originError = assertSameOrigin(request);
|
||||
|
||||
if (originError) {
|
||||
return originError;
|
||||
}
|
||||
|
||||
return proxyHermesRequest(request, "/customers/import/preview");
|
||||
}
|
||||
40
frontend/athena/app/api/customers/import/template/route.ts
Normal file
40
frontend/athena/app/api/customers/import/template/route.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
const columns = [
|
||||
"customer_number",
|
||||
"company_name",
|
||||
"legal_name",
|
||||
"customer_type",
|
||||
"status",
|
||||
"industry",
|
||||
"website",
|
||||
"email",
|
||||
"phone",
|
||||
"tax_number",
|
||||
"vat_id",
|
||||
"notes",
|
||||
"address_type",
|
||||
"street",
|
||||
"postal_code",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"address_is_primary",
|
||||
"contact_first_name",
|
||||
"contact_last_name",
|
||||
"contact_position",
|
||||
"contact_email",
|
||||
"contact_phone",
|
||||
"contact_mobile",
|
||||
"contact_is_primary",
|
||||
"contact_notes",
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
return new NextResponse(`${columns.join(";")}\n`, {
|
||||
headers: {
|
||||
"Content-Disposition": 'attachment; filename="kundenimport-vorlage.csv"',
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Edit, Eye, Plus, Trash2 } from "lucide-react";
|
||||
import { Edit, Eye, Plus, Trash2, Upload } from "lucide-react";
|
||||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
||||
|
|
@ -10,11 +10,12 @@ import SearchInput from "@/components/common/SearchInput";
|
|||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import CustomerFormDialog from "@/components/customers/CustomerFormDialog";
|
||||
import CustomerImportDialog from "@/components/customers/CustomerImportDialog";
|
||||
import CustomerStatusBadge from "@/components/customers/CustomerStatusBadge";
|
||||
import { api } from "@/lib/api";
|
||||
import { hasPermission } from "@/lib/permissions";
|
||||
import type { CurrentUser } from "@/types/rbac";
|
||||
import type { Customer, CustomerPayload, CustomerStatus, CustomerType } from "@/types/customer";
|
||||
import type { Customer, CustomerImportCommitResponse, CustomerPayload, CustomerStatus, CustomerType } from "@/types/customer";
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ export default function CustomersPage() {
|
|||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||
const [page, setPage] = useState(1);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
|
||||
const [deleteCustomer, setDeleteCustomer] = useState<Customer | null>(null);
|
||||
|
||||
|
|
@ -122,6 +124,7 @@ export default function CustomersPage() {
|
|||
const pageCount = Math.max(1, Math.ceil(filteredCustomers.length / pageSize));
|
||||
const currentPage = Math.min(page, pageCount);
|
||||
const pageCustomers = filteredCustomers.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
const canImportCustomers = hasPermission(currentUser, "customers.create") || hasPermission(currentUser, "customers.update");
|
||||
|
||||
const columns: DataTableColumn<Customer>[] = [
|
||||
{ key: "customer_number", label: "Kundennummer", sortable: true, render: (customer) => customer.customer_number },
|
||||
|
|
@ -221,6 +224,16 @@ export default function CustomersPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleImportCommitted(result: CustomerImportCommitResponse) {
|
||||
showToast({
|
||||
type: "success",
|
||||
title: "Kundenimport abgeschlossen",
|
||||
description: `${result.created} erstellt, ${result.updated} aktualisiert, ${result.skipped} übersprungen`,
|
||||
});
|
||||
setLoading(true);
|
||||
await loadCustomers();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
|
|
@ -228,12 +241,20 @@ export default function CustomersPage() {
|
|||
<h1 className="text-3xl font-bold text-slate-950">Kunden</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">{filteredCustomers.length} von {customers.length} Kunden</p>
|
||||
</div>
|
||||
{hasPermission(currentUser, "customers.create") && (
|
||||
<Button type="button" onClick={openCreateDialog}>
|
||||
<Plus size={16} />
|
||||
Neuer Kunde
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canImportCustomers && (
|
||||
<Button type="button" variant="outline" onClick={() => setImportOpen(true)}>
|
||||
<Upload size={16} />
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission(currentUser, "customers.create") && (
|
||||
<Button type="button" onClick={openCreateDialog}>
|
||||
<Plus size={16} />
|
||||
Neuer Kunde
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-white p-4 lg:flex-row lg:items-center">
|
||||
|
|
@ -286,6 +307,13 @@ export default function CustomersPage() {
|
|||
onSubmit={saveCustomer}
|
||||
/>
|
||||
|
||||
<CustomerImportDialog
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onCommitted={handleImportCommitted}
|
||||
onError={(message) => showToast({ type: "error", title: "Kundenimport fehlgeschlagen", description: message })}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(deleteCustomer)}
|
||||
title="Kunde löschen"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue