Compare commits
3 Commits
5c28fbdea5
...
553ec7eadd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553ec7eadd | ||
|
|
4c6fde326b | ||
|
|
23be8bcb52 |
@@ -8,7 +8,7 @@ export default async function AdminOrdersPage() {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-gradient">Bestellungen</h1>
|
<h1 className="text-3xl font-bold tracking-tight text-gradient">Bestellungen</h1>
|
||||||
<p className="text-slate-400">Verwalten Sie alle Kundenbestellungen und greifen Sie auf generierte Angebotsbestätigungen zu.</p>
|
<p className="text-slate-400">Verwalten Sie alle Kundenbestellungen und greifen Sie auf generierte Annfragesbestätigungen zu.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default async function AdminOrdersPage() {
|
|||||||
|
|
||||||
async function OrdersData() {
|
async function OrdersData() {
|
||||||
const adminDb = createAdminClient()
|
const adminDb = createAdminClient()
|
||||||
|
|
||||||
const { data: orders, error } = await adminDb
|
const { data: orders, error } = await adminDb
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { createAdminClient } from "@/lib/supabase/admin";
|
import { createAdminClient } from "@/lib/supabase/admin";
|
||||||
|
import { InvoicePDF } from "@/components/invoice-pdf";
|
||||||
|
import { renderToBuffer } from "@react-pdf/renderer";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
@@ -8,41 +11,92 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const adminDb = createAdminClient();
|
const adminDb = createAdminClient();
|
||||||
|
const inline = request.nextUrl.searchParams.get("inline") === "true";
|
||||||
|
|
||||||
// 1. Get the order from database
|
// 1. Get the order from database
|
||||||
const { data: order, error } = await adminDb
|
const { data: order, error } = await adminDb
|
||||||
.from("orders")
|
.from("orders")
|
||||||
.select("pdf_url, order_number")
|
.select("customer_data, order_data, total_price, created_at, pdf_url, order_number")
|
||||||
.eq("id", id)
|
.eq("id", id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !order || !order.pdf_url) {
|
if (error || !order) {
|
||||||
return new NextResponse("PDF not found", { status: 404 });
|
return new NextResponse("Order not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Determine file name in storage bucket
|
|
||||||
const storageFileName = `ab_${id}.pdf`;
|
const storageFileName = `ab_${id}.pdf`;
|
||||||
|
let fileBuffer: Uint8Array | ArrayBuffer | null = null;
|
||||||
|
|
||||||
// 3. Download file from Supabase storage
|
// 2. Try to download from storage if pdf_url is set
|
||||||
const { data: fileData, error: downloadError } = await adminDb
|
if (order.pdf_url) {
|
||||||
.storage
|
try {
|
||||||
.from("invoices")
|
const { data: fileData, error: downloadError } = await adminDb
|
||||||
.download(storageFileName);
|
.storage
|
||||||
|
.from("invoices")
|
||||||
|
.download(storageFileName);
|
||||||
|
|
||||||
if (downloadError || !fileData) {
|
if (!downloadError && fileData) {
|
||||||
console.error("Failed to download PDF from storage:", downloadError);
|
fileBuffer = await fileData.arrayBuffer();
|
||||||
return new NextResponse("Error downloading file from storage", { status: 500 });
|
}
|
||||||
|
} catch (storageErr) {
|
||||||
|
console.warn("Could not retrieve file from storage, will regenerate:", storageErr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Return as attachment
|
// 3. If file not found or pdf_url not set, generate dynamically
|
||||||
const buffer = await fileData.arrayBuffer();
|
if (!fileBuffer) {
|
||||||
|
try {
|
||||||
|
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffer = await renderToBuffer(
|
||||||
|
React.createElement(InvoicePDF, {
|
||||||
|
orderNumber: order.order_number || `AE-${id.slice(0, 8)}`,
|
||||||
|
dateStr: formattedDate,
|
||||||
|
customer: order.customer_data,
|
||||||
|
orderData: order.order_data,
|
||||||
|
totalPrice: Number(order.total_price),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upload/Upsert to storage bucket
|
||||||
|
const { error: uploadError } = await adminDb.storage
|
||||||
|
.from("invoices")
|
||||||
|
.upload(storageFileName, Buffer.from(buffer), {
|
||||||
|
contentType: "application/pdf",
|
||||||
|
upsert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploadError) {
|
||||||
|
console.error("Failed to upload regenerated PDF:", uploadError);
|
||||||
|
} else {
|
||||||
|
// Update db with pdf_url
|
||||||
|
const path = `invoices/${storageFileName}`;
|
||||||
|
await adminDb.from("orders").update({ pdf_url: path }).eq("id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBuffer = buffer;
|
||||||
|
} catch (genErr: any) {
|
||||||
|
console.error("PDF generation failed inside API:", genErr);
|
||||||
|
return new NextResponse("Error generating PDF: " + genErr.message, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Return as PDF response
|
||||||
const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`;
|
const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`;
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`);
|
if (inline) {
|
||||||
|
headers.append("Content-Disposition", "inline");
|
||||||
|
} else {
|
||||||
|
headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`);
|
||||||
|
}
|
||||||
headers.append("Content-Type", "application/pdf");
|
headers.append("Content-Type", "application/pdf");
|
||||||
|
|
||||||
return new NextResponse(buffer, {
|
return new NextResponse(fileBuffer as any, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ export default function Impressum() {
|
|||||||
<h2 className="text-xl font-bold text-white">Haftungsausschluss</h2>
|
<h2 className="text-xl font-bold text-white">Haftungsausschluss</h2>
|
||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h3 className="text-md font-semibold text-slate-200">1. Inhalt des Onlineangebotes</h3>
|
<h3 className="text-md font-semibold text-slate-200">1. Inhalt der Onlineanfrage</h3>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">
|
<p className="text-sm text-slate-400 leading-relaxed">
|
||||||
Der Autor übernimmt keinerlei Gewähr für die Aktualität, Korrektheit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche gegen den Autor, welche sich auf Schäden materieller oder ideeller Art beziehen, die durch die Nutzung oder Nichtnutzung der dargebotenen Informationen bzw. durch die Nutzung fehlerhafter und unvollständiger Informationen verursacht wurden, sind grundsätzlich ausgeschlossen, sofern seitens des Autors kein nachweislich vorsätzliches oder grob fahrlässiges Verschulden vorliegt.
|
Der Autor übernimmt keinerlei Gewähr für die Aktualität, Korrektheit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche gegen den Autor, welche sich auf Schäden materieller oder ideeller Art beziehen, die durch die Nutzung oder Nichtnutzung der dargebotenen Informationen bzw. durch die Nutzung fehlerhafter und unvollständiger Informationen verursacht wurden, sind grundsätzlich ausgeschlossen, sofern seitens des Autors kein nachweislich vorsätzliches oder grob fahrlässiges Verschulden vorliegt.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">
|
<p className="text-sm text-slate-400 leading-relaxed">
|
||||||
Alle Angebote sind freibleibend und unverbindlich. Der Autor behält es sich ausdrücklich vor, Teile der Seiten oder das gesamte Angebot ohne gesonderte Ankündigung zu verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.
|
Alle Anfragen sind freibleibend und unverbindlich. Der Autor behält es sich ausdrücklich vor, Teile der Seiten oder die gesamte Anfrage ohne gesonderte Ankündigung zu verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -93,14 +93,14 @@ export default function Impressum() {
|
|||||||
Bei direkten oder indirekten Verweisen auf fremde Webseiten („Hyperlinks“), die außerhalb des Verantwortungsbereiches des Autors liegen, würde eine Haftungsverpflichtung ausschließlich in dem Fall in Kraft treten, in dem der Autor von den Inhalten Kenntnis hat und es ihm technisch möglich und zumutbar wäre, die Nutzung im Falle rechtswidriger Inhalte zu verhindern. Der Autor erklärt hiermit ausdrücklich, dass zum Zeitpunkt der Linksetzung keine illegalen Inhalte auf den zu verlinkenden Seiten erkennbar waren.
|
Bei direkten oder indirekten Verweisen auf fremde Webseiten („Hyperlinks“), die außerhalb des Verantwortungsbereiches des Autors liegen, würde eine Haftungsverpflichtung ausschließlich in dem Fall in Kraft treten, in dem der Autor von den Inhalten Kenntnis hat und es ihm technisch möglich und zumutbar wäre, die Nutzung im Falle rechtswidriger Inhalte zu verhindern. Der Autor erklärt hiermit ausdrücklich, dass zum Zeitpunkt der Linksetzung keine illegalen Inhalte auf den zu verlinkenden Seiten erkennbar waren.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">
|
<p className="text-sm text-slate-400 leading-relaxed">
|
||||||
Auf die aktuelle und zukünftige Gestaltung, die Inhalte oder die Urheberschaft der gelinkten/verknüpften Seiten hat der Autor keinerlei Einfluss. Deshalb distanziert er sich hiermit ausdrücklich von allen Inhalten aller gelinkten /verknüpften Seiten, die nach der Linksetzung verändert wurden. Diese Feststellung gilt für alle innerhalb des eigenen Internetangebotes gesetzten Links und Verweise sowie für Fremdeinträge in vom Autor eingerichteten Gästebüchern, Diskussionsforen und Mailinglisten. Für illegale, fehlerhafte oder unvollständige Inhalte und insbesondere für Schäden, die aus der Nutzung oder Nichtnutzung solcherart dargebotener Informationen entstehen, haftet allein der Anbieter der Seite, auf welche verwiesen wurde, nicht derjenige, der über Links auf die jeweilige Veröffentlichung lediglich verweist.
|
Auf die aktuelle und zukünftige Gestaltung, die Inhalte oder die Urheberschaft der gelinkten/verknüpften Seiten hat der Autor keinerlei Einfluss. Deshalb distanziert er sich hiermit ausdrücklich von allen Inhalten aller gelinkten /verknüpften Seiten, die nach der Linksetzung verändert wurden. Diese Feststellung gilt für alle innerhalb des eigenen Internetanfrage gesetzten Links und Verweise sowie für Fremdeinträge in vom Autor eingerichteten Gästebüchern, Diskussionsforen und Mailinglisten. Für illegale, fehlerhafte oder unvollständige Inhalte und insbesondere für Schäden, die aus der Nutzung oder Nichtnutzung solcherart dargebotener Informationen entstehen, haftet allein der Anbieter der Seite, auf welche verwiesen wurde, nicht derjenige, der über Links auf die jeweilige Veröffentlichung lediglich verweist.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h3 className="text-md font-semibold text-slate-200">4. Rechtswirksamkeit dieses Haftungsausschlusses</h3>
|
<h3 className="text-md font-semibold text-slate-200">4. Rechtswirksamkeit dieses Haftungsausschlusses</h3>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">
|
<p className="text-sm text-slate-400 leading-relaxed">
|
||||||
Dieser Haftungsausschluss ist als Teil des Internetangebotes zu betrachten, von dem aus auf diese Seite verwiesen wurde. Sofern Teile oder einzelne Formulierungen dieses Textes der geltenden Rechtslage nicht, nicht mehr oder nicht vollständig entsprechen sollten, bleiben die übrigen Teile des Dokumentes in ihrem Inhalt und ihrer Gültigkeit davon unberührt.
|
Dieser Haftungsausschluss ist als Teil der Onlineanfrage zu betrachten, von dem aus auf diese Seite verwiesen wurde. Sofern Teile oder einzelne Formulierungen dieses Textes der geltenden Rechtslage nicht, nicht mehr oder nicht vollständig entsprechen sollten, bleiben die übrigen Teile des Dokumentes in ihrem Inhalt und ihrer Gültigkeit davon unberührt.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export default async function OrderSuccessPage({
|
|||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<Button variant="outline" size="sm" asChild className="border-white/10 hover:bg-white/10">
|
<Button variant="outline" size="sm" asChild className="border-white/10 hover:bg-white/10">
|
||||||
<a href={resolveSupabaseUrl(o.pdf_url)} target="_blank" rel="noopener noreferrer">
|
<a href={resolveSupabaseUrl(o.pdf_url)} target="_blank" rel="noopener noreferrer">
|
||||||
<ExternalLink className="w-4 h-4 mr-2" /> Angebotsbestätigung ansehen
|
<ExternalLink className="w-4 h-4 mr-2" /> Anfragebestätigung ansehen
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" asChild>
|
<Button variant="secondary" size="sm" asChild>
|
||||||
|
|||||||
@@ -118,9 +118,8 @@ export function SetupWizard() {
|
|||||||
{[1, 2, 3].map((s) => (
|
{[1, 2, 3].map((s) => (
|
||||||
<div
|
<div
|
||||||
key={s}
|
key={s}
|
||||||
className={`h-1.5 rounded-full transition-all duration-300 ${
|
className={`h-1.5 rounded-full transition-all duration-300 ${s === step ? 'w-8 bg-blue-500' : 'w-2 bg-slate-800'
|
||||||
s === step ? 'w-8 bg-blue-500' : 'w-2 bg-slate-800'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +142,7 @@ export function SetupWizard() {
|
|||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<h1 className="text-3xl font-extrabold tracking-tight">Willkommen bei CASPOS!</h1>
|
<h1 className="text-3xl font-extrabold tracking-tight">Willkommen bei CASPOS!</h1>
|
||||||
<p className="text-slate-400 leading-relaxed">
|
<p className="text-slate-400 leading-relaxed">
|
||||||
Richten Sie Ihren persönlichen Lizenz- und Angebots-Shop in wenigen Schritten ein. Wir konfigurieren Ihr Administrator-Konto und die Mailverbindung.
|
Richten Sie Ihren persönlichen Lizenz- und Anfrage-Shop in wenigen Schritten ein. Wir konfigurieren Ihr Administrator-Konto und die Mailverbindung.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ export function SetupWizard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="w-4 h-4 text-green-400 shrink-0" />
|
<Check className="w-4 h-4 text-green-400 shrink-0" />
|
||||||
<span>SMTP Mailversand für direkte Angebotsbestätigungen</span>
|
<span>SMTP Mailversand für direkte Anfragebestätigungen</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -223,22 +223,16 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
|
|||||||
Bearbeiten
|
Bearbeiten
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
{order.pdf_url ? (
|
<Button variant="outline" size="sm" asChild className="h-8 border-white/10 hover:bg-primary/20 text-xs">
|
||||||
<>
|
<a href={`/api/admin/orders/${order.id}/download?inline=true`} target="_blank" rel="noopener noreferrer">
|
||||||
<Button variant="outline" size="sm" asChild className="h-8 border-white/10 hover:bg-primary/20 text-xs">
|
<ExternalLink className="w-3.5 h-3.5 mr-1" /> PDF
|
||||||
<a href={resolveSupabaseUrl(order.pdf_url)} target="_blank" rel="noopener noreferrer">
|
</a>
|
||||||
<ExternalLink className="w-3.5 h-3.5 mr-1" /> PDF
|
</Button>
|
||||||
</a>
|
<Button variant="secondary" size="sm" asChild className="h-8 text-xs">
|
||||||
</Button>
|
<a href={`/api/admin/orders/${order.id}/download`}>
|
||||||
<Button variant="secondary" size="sm" asChild className="h-8 text-xs">
|
<Download className="w-3.5 h-3.5" />
|
||||||
<a href={`/api/admin/orders/${order.id}/download`}>
|
</a>
|
||||||
<Download className="w-3.5 h-3.5" />
|
</Button>
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-slate-500 italic self-center px-2">Keine Bestätigung</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export function OrderWizard({
|
|||||||
router.push(`/order/success?id=${order.id}`)
|
router.push(`/order/success?id=${order.id}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
alert(error?.message || (initialOrder ? 'Fehler beim Aktualisieren des Angebots.' : 'Fehler bei der Bestellung. Bitte versuchen Sie es erneut.'))
|
alert(error?.message || (initialOrder ? 'Fehler beim Aktualisieren der Anfrage.' : 'Fehler bei der Bestellung. Bitte versuchen Sie es erneut.'))
|
||||||
setIsSubmitting(false) // Nur bei Fehler wieder entsperren
|
setIsSubmitting(false) // Nur bei Fehler wieder entsperren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,8 +447,8 @@ export function OrderWizard({
|
|||||||
<div key={s} className="relative z-10 flex flex-col items-center gap-2">
|
<div key={s} className="relative z-10 flex flex-col items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${step >= s
|
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${step >= s
|
||||||
? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]'
|
? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]'
|
||||||
: 'bg-slate-800 text-slate-400'
|
: 'bg-slate-800 text-slate-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step > s ? <Check className="w-6 h-6" /> : s}
|
{step > s ? <Check className="w-6 h-6" /> : s}
|
||||||
@@ -534,8 +534,8 @@ export function OrderWizard({
|
|||||||
<label
|
<label
|
||||||
key={customer.id}
|
key={customer.id}
|
||||||
className={`flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${selectedEndCustomerId === customer.id
|
className={`flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${selectedEndCustomerId === customer.id
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -648,8 +648,8 @@ export function OrderWizard({
|
|||||||
<div
|
<div
|
||||||
onClick={() => handleBillingIntervalChange('one_time')}
|
onClick={() => handleBillingIntervalChange('one_time')}
|
||||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'one_time'
|
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'one_time'
|
||||||
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
||||||
: 'border-white/5 bg-white/5 hover:border-white/20'
|
: 'border-white/5 bg-white/5 hover:border-white/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -674,8 +674,8 @@ export function OrderWizard({
|
|||||||
<div
|
<div
|
||||||
onClick={() => handleBillingIntervalChange('monthly')}
|
onClick={() => handleBillingIntervalChange('monthly')}
|
||||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'monthly'
|
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'monthly'
|
||||||
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
||||||
: 'border-white/5 bg-white/5 hover:border-white/20'
|
: 'border-white/5 bg-white/5 hover:border-white/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -1271,12 +1271,12 @@ export function OrderWizard({
|
|||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
initialOrder ? (
|
initialOrder ? (
|
||||||
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Angebot wird aktualisiert...</>
|
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird aktualisiert...</>
|
||||||
) : (
|
) : (
|
||||||
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird versendet...</>
|
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird versendet...</>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
initialOrder ? 'Angebot aktualisieren' : 'Anfrage versenden'
|
initialOrder ? 'Anfrage aktualisieren' : 'Anfrage versenden'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" className="w-full text-white" onClick={prevStep}>
|
<Button variant="ghost" className="w-full text-white" onClick={prevStep}>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { getProducts, getCategories } from '@/lib/actions/products'
|
|||||||
function generateOrderNumber(): string {
|
function generateOrderNumber(): string {
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const rand = Math.floor(10000 + Math.random() * 90000) // 5-stellig, niemals < 10000
|
const rand = Math.floor(10000 + Math.random() * 90000) // 5-stellig, niemals < 10000
|
||||||
return `BE-${year}-${rand}`
|
return `AE-${year}-${rand}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,7 +315,7 @@ export async function submitOrder(params: {
|
|||||||
await sendMail({
|
await sendMail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: `Anfragebestätigung ${orderNumber}`,
|
subject: `Anfragebestätigung ${orderNumber}`,
|
||||||
text: `Hallo,\n\nvielen Dank für Ihre Anfrage bei CASPOS Shop!\n\nAngebotsdetails:\n- Anfrage-Nummer: ${orderNumber}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
text: `Hallo,\n\nvielen Dank für Ihre Anfrage bei CASPOS Shop!\n\nAnfragedetails:\n- Anfrage-Nummer: ${orderNumber}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Vielen Dank für Ihre Anfrage!</h2>
|
<h2 style="color: #0f172a; margin-bottom: 16px;">Vielen Dank für Ihre Anfrage!</h2>
|
||||||
@@ -347,7 +347,7 @@ export async function submitOrder(params: {
|
|||||||
`,
|
`,
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: `Angebotsbestaetigung_${orderNumber}.pdf`,
|
filename: `Anfragebestaetigung_${orderNumber}.pdf`,
|
||||||
content: buffer,
|
content: buffer,
|
||||||
contentType: 'application/pdf',
|
contentType: 'application/pdf',
|
||||||
}
|
}
|
||||||
@@ -499,11 +499,11 @@ export async function updateOrder(
|
|||||||
const isOwner = existingOrder.user_id === user.id
|
const isOwner = existingOrder.user_id === user.id
|
||||||
|
|
||||||
if (!isAdmin && !isOwner) {
|
if (!isAdmin && !isOwner) {
|
||||||
throw new Error('Keine Berechtigung dieses Angebot zu bearbeiten.')
|
throw new Error('Keine Berechtigung dieses Anfrage zu bearbeiten.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingOrder.status === 'completed' && !isAdmin) {
|
if (existingOrder.status === 'completed' && !isAdmin) {
|
||||||
throw new Error('Abgeschlossene Angebote können nicht mehr bearbeitet werden.')
|
throw new Error('Abgeschlossene Anfragen können nicht mehr bearbeitet werden.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch catalog directly from DB to prevent client tampering
|
// Fetch catalog directly from DB to prevent client tampering
|
||||||
@@ -720,13 +720,13 @@ export async function updateOrder(
|
|||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
to: orderUserEmail,
|
to: orderUserEmail,
|
||||||
subject: `Angebotsänderung ${order.order_number}`,
|
subject: `Anfrageänderung ${order.order_number}`,
|
||||||
text: `Hallo,\n\nihr Angebot wurde erfolgreich aktualisiert!\n\nNeue Angebotsdetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihr aktualisiertes Angebot als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
text: `Hallo,\n\nihre Anfrage wurde erfolgreich aktualisiert!\n\nNeue Anfragedetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfrage als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Ihr Angebot wurde aktualisiert!</h2>
|
<h2 style="color: #0f172a; margin-bottom: 16px;">Ihre Anfrage wurde aktualisiert!</h2>
|
||||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Ihr Angebot mit der Nummer ${order.order_number} wurde erfolgreich aktualisiert.</p>
|
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Ihr Anfrage mit der Nummer ${order.order_number} wurde erfolgreich aktualisiert.</p>
|
||||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||||
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">Neue Anfrage-Details</h3>
|
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">Neue Anfrage-Details</h3>
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||||
@@ -751,7 +751,7 @@ export async function updateOrder(
|
|||||||
`,
|
`,
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: `Angebotsbestaetigung_${order.order_number}.pdf`,
|
filename: `Anfragebestaetigung_${order.order_number}.pdf`,
|
||||||
content: buffer,
|
content: buffer,
|
||||||
contentType: 'application/pdf',
|
contentType: 'application/pdf',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user