feat(admin): edit order status with dropdown and notify partner by email
Some checks failed
Staging Build / build (push) Has been cancelled
Some checks failed
Staging Build / build (push) Has been cancelled
This commit is contained in:
@@ -1,11 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Download, ExternalLink, Search } from "lucide-react";
|
import { Download, ExternalLink, Search, Loader2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { updateOrderStatus } from "@/lib/actions/orders";
|
||||||
|
|
||||||
interface OrdersTableProps {
|
interface OrdersTableProps {
|
||||||
initialOrders: any[];
|
initialOrders: any[];
|
||||||
@@ -26,10 +33,29 @@ const statusClass: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function OrdersTable({ initialOrders }: OrdersTableProps) {
|
export function OrdersTable({ initialOrders }: OrdersTableProps) {
|
||||||
|
const [orders, setOrders] = useState(initialOrders);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||||
|
const [updatingId, setUpdatingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredOrders = initialOrders.filter((order) => {
|
useEffect(() => {
|
||||||
|
setOrders(initialOrders);
|
||||||
|
}, [initialOrders]);
|
||||||
|
|
||||||
|
const handleStatusChange = async (orderId: string, newStatus: 'pending' | 'active' | 'completed' | 'cancelled') => {
|
||||||
|
setUpdatingId(orderId);
|
||||||
|
try {
|
||||||
|
await updateOrderStatus(orderId, newStatus);
|
||||||
|
setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("Fehler beim Ändern des Status.");
|
||||||
|
} finally {
|
||||||
|
setUpdatingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredOrders = orders.filter((order) => {
|
||||||
const orderNum = (order.order_number || "").toLowerCase();
|
const orderNum = (order.order_number || "").toLowerCase();
|
||||||
const company = (order.customer_data?.company_name || "").toLowerCase();
|
const company = (order.customer_data?.company_name || "").toLowerCase();
|
||||||
const contact = `${order.customer_data?.first_name || ""} ${order.customer_data?.last_name || ""}`.toLowerCase();
|
const contact = `${order.customer_data?.first_name || ""} ${order.customer_data?.last_name || ""}`.toLowerCase();
|
||||||
@@ -163,9 +189,31 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
|
|||||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
|
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={`text-xs px-2 py-0.5 rounded ${statusClass[order.status] ?? 'bg-slate-500/20 text-slate-300'}`}>
|
<DropdownMenu>
|
||||||
{statusLabel[order.status] ?? order.status}
|
<DropdownMenuTrigger asChild disabled={updatingId === order.id}>
|
||||||
</Badge>
|
<button className="focus:outline-none focus:ring-0 active:scale-95 disabled:pointer-events-none transition-transform duration-100">
|
||||||
|
<Badge className={`text-xs px-2 py-0.5 rounded cursor-pointer transition-all duration-200 hover:opacity-85 flex items-center gap-1.5 ${statusClass[order.status] ?? 'bg-slate-500/20 text-slate-300'}`}>
|
||||||
|
{updatingId === order.id ? (
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||||
|
) : null}
|
||||||
|
{statusLabel[order.status] ?? order.status}
|
||||||
|
<span className="text-[9px] opacity-70">▼</span>
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="bg-slate-900 border-white/10 text-white min-w-[140px]">
|
||||||
|
{Object.entries(statusLabel).map(([key, label]) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={key}
|
||||||
|
disabled={updatingId === order.id}
|
||||||
|
onClick={() => handleStatusChange(order.id, key as any)}
|
||||||
|
className={`text-xs cursor-pointer hover:bg-white/10 transition-colors focus:bg-white/10 focus:text-white ${order.status === key ? 'font-bold text-primary' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{order.pdf_url ? (
|
{order.pdf_url ? (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
import { createAdminClient } from '@/lib/supabase/admin'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { sendMail } from '@/utils/mail'
|
import { sendMail } from '@/utils/mail'
|
||||||
import { InvoicePDF } from '@/components/invoice-pdf'
|
import { InvoicePDF } from '@/components/invoice-pdf'
|
||||||
@@ -280,3 +281,96 @@ export async function submitOrder(params: {
|
|||||||
revalidatePath('/my-orders')
|
revalidatePath('/my-orders')
|
||||||
return order as Order
|
return order as Order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateOrderStatus(
|
||||||
|
orderId: string,
|
||||||
|
newStatus: 'pending' | 'active' | 'completed' | 'cancelled'
|
||||||
|
) {
|
||||||
|
const admin = createAdminClient()
|
||||||
|
|
||||||
|
// 1. Fetch current order
|
||||||
|
const { data: order, error: fetchError } = await admin
|
||||||
|
.from('orders')
|
||||||
|
.select('id, user_id, order_number, status')
|
||||||
|
.eq('id', orderId)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (fetchError || !order) {
|
||||||
|
throw new Error(`Bestellung nicht gefunden: ${fetchError?.message || 'Unbekannt'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = order.status
|
||||||
|
|
||||||
|
if (oldStatus === newStatus) {
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update order status
|
||||||
|
const { data: updatedOrder, error: updateError } = await admin
|
||||||
|
.from('orders')
|
||||||
|
.update({ status: newStatus })
|
||||||
|
.eq('id', orderId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (updateError || !updatedOrder) {
|
||||||
|
throw new Error(`Fehler beim Aktualisieren des Status: ${updateError?.message || 'Unbekannt'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Send email to the retailer (händler)
|
||||||
|
if (order.user_id) {
|
||||||
|
try {
|
||||||
|
const { data: { user }, error: userError } = await admin.auth.admin.getUserById(order.user_id)
|
||||||
|
|
||||||
|
if (!userError && user && user.email) {
|
||||||
|
const orderNumber = order.order_number || order.id.slice(0, 8)
|
||||||
|
const statusLabelMap: Record<string, string> = {
|
||||||
|
pending: 'Eingegangen',
|
||||||
|
active: 'In Bearbeitung',
|
||||||
|
completed: 'Abgeschlossen',
|
||||||
|
cancelled: 'Storniert',
|
||||||
|
}
|
||||||
|
const oldLabel = statusLabelMap[oldStatus] || oldStatus
|
||||||
|
const newLabel = statusLabelMap[newStatus] || newStatus
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: user.email,
|
||||||
|
subject: `Statusänderung Ihrer Bestellung ${orderNumber}`,
|
||||||
|
text: `Hallo,\n\nder Status Ihrer Bestellung mit der Nummer ${orderNumber} hat sich geändert.\n\nNeuer Status: ${newLabel} (vorher: ${oldLabel})\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||||
|
html: `
|
||||||
|
<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;">Status Ihrer Bestellung hat sich geändert</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;">der Status Ihrer Bestellung mit der Nummer <strong>${orderNumber}</strong> wurde aktualisiert.</p>
|
||||||
|
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Bestellnummer:</td>
|
||||||
|
<td style="padding: 4px 0;">${orderNumber}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-weight: bold;">Alter Status:</td>
|
||||||
|
<td style="padding: 4px 0; text-decoration: line-through; color: #94a3b8;">${oldLabel}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 4px 0; font-weight: bold;">Neuer Status:</td>
|
||||||
|
<td style="padding: 4px 0; font-weight: bold; color: #3b82f6;">${newLabel}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Viele Grüße,<br>Ihr CASPOS Shop-Team</p>
|
||||||
|
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
|
||||||
|
<p style="color: #94a3b8; font-size: 12px; text-align: center;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (mailError) {
|
||||||
|
console.error('Failed to send status update email:', mailError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/orders')
|
||||||
|
revalidatePath('/my-orders')
|
||||||
|
return updatedOrder as Order
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user