mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
added billing creation
This commit is contained in:
parent
0af1f37286
commit
0d1eb75127
40 changed files with 2040 additions and 152 deletions
9
app/admin/clients/[id]/billing/page.tsx
Normal file
9
app/admin/clients/[id]/billing/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Params } from "@/modules/shared/types/user"
|
||||
import { BillingPanel } from "@/modules/shared/ui/views/billing-panel"
|
||||
|
||||
const BillingAdminPage = async ({ params } : { params: Params }) => {
|
||||
const { id } = await params
|
||||
return <BillingPanel profileId={id} isAdmin />
|
||||
}
|
||||
|
||||
export default BillingAdminPage
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
import StammdatenAdminViewPage from "@/modules/admin/ui/views/stammdaten-admin-view"
|
||||
|
||||
type Params = Promise<{ id: string }>;
|
||||
import { getStammdaten } from "@/modules/shared/queries/stammdaten"
|
||||
import { Params } from "@/modules/shared/types/user"
|
||||
import { StammdatenPanel } from "@/modules/shared/ui/views/stammdaten-panel"
|
||||
|
||||
const DataAdminPage = async ({ params }: { params: Params }) => {
|
||||
const { id } = await params
|
||||
return <StammdatenAdminViewPage clientProfileId={id} />
|
||||
const stammdaten = await getStammdaten(id)
|
||||
|
||||
return (
|
||||
<StammdatenPanel
|
||||
profileId={id}
|
||||
client={stammdaten?.client ?? null}
|
||||
paymentDetails={stammdaten?.paymentDetails ?? null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataAdminPage
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { notFound, redirect } from "next/navigation";
|
||||
import { createAdminClient } from "@/lib/supabase/admin";
|
||||
import { verifySession } from "@/lib/supabase/dal";
|
||||
|
||||
type Params = Promise<{ id: string }>;
|
||||
import { Params } from "@/modules/shared/types/user";
|
||||
|
||||
export default async function ClientDetailLayout({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { createAdminClient } from "@/lib/supabase/admin";
|
||||
import { Params } from "@/modules/shared/types/user";
|
||||
|
||||
type Params = Promise<{ id: string }>;
|
||||
|
||||
const ClientDetailPage = async ({ params }: { params: Params }) => {
|
||||
const { id } = await params;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import PasswordAdminViewPage from "@/modules/admin/ui/views/password-admin-view"
|
||||
|
||||
type Params = Promise<{id: string}>
|
||||
import { Params } from "@/modules/shared/types/user"
|
||||
import { CredentialsPanel } from "@/modules/shared/ui/views/credentials-panel"
|
||||
|
||||
const PasswordAdminPage = async ({ params } : { params: Params }) => {
|
||||
const { id } = await params
|
||||
return <PasswordAdminViewPage clientProfileId={id}/>
|
||||
return <CredentialsPanel profileId={id} isAdmin />
|
||||
}
|
||||
|
||||
export default PasswordAdminPage
|
||||
export default PasswordAdminPage
|
||||
|
|
|
|||
45
app/api/invoices/pdf/route.ts
Normal file
45
app/api/invoices/pdf/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import * as z from "zod"
|
||||
import { zipSync } from "fflate"
|
||||
import type { NextRequest } from "next/server"
|
||||
import { getSession } from "@/lib/supabase/dal"
|
||||
import { getInvoicesForPdf } from "@/modules/shared/queries/invoices"
|
||||
import { invoiceFileName, renderInvoicePdf } from "@/modules/shared/pdf/render-invoice"
|
||||
|
||||
const IdsSchema = z.array(z.string().uuid()).min(1).max(100)
|
||||
|
||||
// Zugriff regelt RLS: Clients bekommen nur eigene, nicht stornierte Rechnungen zurück.
|
||||
export async function GET(request: NextRequest) {
|
||||
const user = await getSession()
|
||||
if (!user) return Response.json({ error: "Nicht angemeldet." }, { status: 401 })
|
||||
|
||||
const parsed = IdsSchema.safeParse(request.nextUrl.searchParams.getAll("id"))
|
||||
if (!parsed.success) return Response.json({ error: "Ungültige Auswahl." }, { status: 400 })
|
||||
|
||||
const invoices = await getInvoicesForPdf(parsed.data)
|
||||
if (invoices.length === 0) return Response.json({ error: "Keine Rechnungen gefunden." }, { status: 404 })
|
||||
|
||||
if (invoices.length === 1) {
|
||||
const pdf = await renderInvoicePdf(invoices[0])
|
||||
const disposition = request.nextUrl.searchParams.has("inline") ? "inline" : "attachment"
|
||||
return new Response(new Uint8Array(pdf), {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `${disposition}; filename="${invoiceFileName(invoices[0].invoiceNumber)}"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const files: Record<string, Uint8Array> = {}
|
||||
for (const invoice of invoices) {
|
||||
files[invoiceFileName(invoice.invoiceNumber)] = new Uint8Array(await renderInvoicePdf(invoice))
|
||||
}
|
||||
|
||||
return new Response(zipSync(files, { level: 0 }), {
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": "attachment; filename=\"Rechnungen.zip\"",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
}
|
||||
41
app/api/invoices/preview/route.ts
Normal file
41
app/api/invoices/preview/route.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import * as z from "zod"
|
||||
import { getProfile } from "@/lib/supabase/dal"
|
||||
import { InvoiceDraftSchema, firstIssueMessage } from "@/modules/shared/schemas/invoice"
|
||||
import { renderInvoicePdf } from "@/modules/shared/pdf/render-invoice"
|
||||
|
||||
const PreviewSchema = z.object({
|
||||
draft: InvoiceDraftSchema,
|
||||
invoiceNumber: z.string().trim().min(1).max(40),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const profile = await getProfile()
|
||||
if (!profile) return Response.json({ error: "Nicht angemeldet." }, { status: 401 })
|
||||
if (profile.role !== "admin") return Response.json({ error: "Keine Berechtigung." }, { status: 403 })
|
||||
|
||||
const parsed = PreviewSchema.safeParse(await request.json().catch(() => null))
|
||||
if (!parsed.success) {
|
||||
return Response.json({ error: firstIssueMessage(parsed.error) }, { status: 400 })
|
||||
}
|
||||
|
||||
const { draft, invoiceNumber } = parsed.data
|
||||
const pdf = await renderInvoicePdf({
|
||||
invoiceNumber,
|
||||
invoiceDate: draft.invoiceDate,
|
||||
dueDate: draft.dueDate,
|
||||
companyName: draft.companyName,
|
||||
billingStreet: draft.billingStreet,
|
||||
billingPostalCode: draft.billingPostalCode,
|
||||
billingCity: draft.billingCity,
|
||||
billingCountry: draft.billingCountry,
|
||||
items: draft.items,
|
||||
})
|
||||
|
||||
return new Response(new Uint8Array(pdf), {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": "inline; filename=\"Vorschau.pdf\"",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
}
|
||||
9
app/client/billing/page.tsx
Normal file
9
app/client/billing/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { verifySession } from "@/lib/supabase/dal"
|
||||
import { BillingPanel } from "@/modules/shared/ui/views/billing-panel";
|
||||
|
||||
const BillingPage = async () => {
|
||||
const session = await verifySession();
|
||||
return <BillingPanel profileId={session.userId} isAdmin={false}/>
|
||||
}
|
||||
|
||||
export default BillingPage
|
||||
|
|
@ -1,7 +1,18 @@
|
|||
import StammdatenClientViewPage from "@/modules/client/ui/views/stammdaten-client-view"
|
||||
import { verifySession } from "@/lib/supabase/dal"
|
||||
import { getStammdaten } from "@/modules/shared/queries/stammdaten"
|
||||
import { StammdatenPanel } from "@/modules/shared/ui/views/stammdaten-panel"
|
||||
|
||||
const DataPage = () => {
|
||||
return <StammdatenClientViewPage/>
|
||||
const DataPage = async () => {
|
||||
const session = await verifySession()
|
||||
const stammdaten = await getStammdaten(session.userId)
|
||||
|
||||
return (
|
||||
<StammdatenPanel
|
||||
profileId={session.userId}
|
||||
client={stammdaten?.client ?? null}
|
||||
paymentDetails={stammdaten?.paymentDetails ?? null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataPage
|
||||
export default DataPage
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import PasswordClientViewPage from "@/modules/client/ui/views/password-client-view"
|
||||
import { verifySession } from "@/lib/supabase/dal"
|
||||
import { CredentialsPanel } from "@/modules/shared/ui/views/credentials-panel"
|
||||
|
||||
const PasswordPage = () => {
|
||||
return <PasswordClientViewPage/>
|
||||
const PasswordPage = async () => {
|
||||
const session = await verifySession()
|
||||
return <CredentialsPanel profileId={session.userId} isAdmin={false} />
|
||||
}
|
||||
|
||||
export default PasswordPage
|
||||
export default PasswordPage
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue