Compare commits

..

2 Commits

Author SHA1 Message Date
DanielS
10f8683d7f style(admin): improve company dropdown layout in user list component
All checks were successful
Staging Build / build (push) Successful in 3m29s
2026-06-30 15:45:00 +02:00
DanielS
19e9733144 feat(admin): add API endpoint to download order confirmation PDF securely as attachment 2026-06-30 15:36:42 +02:00
3 changed files with 119 additions and 69 deletions

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { createAdminClient } from "@/lib/supabase/admin";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const adminDb = createAdminClient();
// 1. Get the order from database
const { data: order, error } = await adminDb
.from("orders")
.select("pdf_url, order_number")
.eq("id", id)
.single();
if (error || !order || !order.pdf_url) {
return new NextResponse("PDF not found", { status: 404 });
}
// 2. Determine file name in storage bucket
const storageFileName = `ab_${id}.pdf`;
// 3. Download file from Supabase storage
const { data: fileData, error: downloadError } = await adminDb
.storage
.from("invoices")
.download(storageFileName);
if (downloadError || !fileData) {
console.error("Failed to download PDF from storage:", downloadError);
return new NextResponse("Error downloading file from storage", { status: 500 });
}
// 4. Return as attachment
const buffer = await fileData.arrayBuffer();
const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`;
const headers = new Headers();
headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`);
headers.append("Content-Type", "application/pdf");
return new NextResponse(buffer, {
status: 200,
headers,
});
} catch (err: any) {
console.error("Download endpoint failed:", err);
return new NextResponse(err.message || "Internal Server Error", { status: 500 });
}
}

View File

@@ -224,7 +224,7 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
</a> </a>
</Button> </Button>
<Button variant="secondary" size="sm" asChild className="h-8 text-xs"> <Button variant="secondary" size="sm" asChild className="h-8 text-xs">
<a href={order.pdf_url} download> <a href={`/api/admin/orders/${order.id}/download`}>
<Download className="w-3.5 h-3.5" /> <Download className="w-3.5 h-3.5" />
</a> </a>
</Button> </Button>

View File

@@ -135,12 +135,12 @@ export function UserList({ initialUsers, companies }: UserListProps) {
const displayUsers = groupByCompany const displayUsers = groupByCompany
? [...filteredUsers].sort((a, b) => { ? [...filteredUsers].sort((a, b) => {
const nameA = a.company_name || 'Keine Firma' const nameA = a.company_name || 'Keine Firma'
const nameB = b.company_name || 'Keine Firma' const nameB = b.company_name || 'Keine Firma'
if (!a.company_name && b.company_name) return 1 if (!a.company_name && b.company_name) return 1
if (a.company_name && !b.company_name) return -1 if (a.company_name && !b.company_name) return -1
return nameA.localeCompare(nameB) return nameA.localeCompare(nameB)
}) })
: filteredUsers : filteredUsers
console.log('COMPANIES IN USERLIST:', companies) console.log('COMPANIES IN USERLIST:', companies)
@@ -286,13 +286,12 @@ export function UserList({ initialUsers, companies }: UserListProps) {
<select <select
value={role} value={role}
onChange={(e) => handleRoleChange(user.id, e.target.value as Role)} onChange={(e) => handleRoleChange(user.id, e.target.value as Role)}
className={`text-xs font-semibold rounded px-3 py-1.5 cursor-pointer focus:outline-none focus:ring-2 transition-colors bg-white dark:bg-slate-950/80 ${ className={`text-xs font-semibold rounded px-3 py-1.5 cursor-pointer focus:outline-none focus:ring-2 transition-colors bg-white dark:bg-slate-950/80 ${role === 'admin'
role === 'admin'
? 'text-purple-600 dark:text-purple-300 border border-purple-500/40 focus:ring-purple-500/50' ? 'text-purple-600 dark:text-purple-300 border border-purple-500/40 focus:ring-purple-500/50'
: role === 'gesperrt' : role === 'gesperrt'
? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50' ? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50'
: 'text-blue-600 dark:text-blue-300 border border-blue-500/40 focus:ring-blue-500/50' : 'text-blue-600 dark:text-blue-300 border border-blue-500/40 focus:ring-blue-500/50'
}`} }`}
style={{ appearance: 'auto' }} style={{ appearance: 'auto' }}
> >
<option value="admin" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Admin</option> <option value="admin" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Admin</option>
@@ -322,42 +321,40 @@ export function UserList({ initialUsers, companies }: UserListProps) {
)} )}
</div> </div>
{companyDropdownOpen === user.id && ( {companyDropdownOpen === user.id && (
<div className="absolute z-50 mt-1 right-0 w-56 bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-lg shadow-xl overflow-hidden"> <div className="absolute z-50 left-0 right-0 mt-1 bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-md shadow-lg p-1 flex flex-col max-h-60 overflow-y-auto">
<div className="p-2 border-b border-slate-200 dark:border-white/10"> <div className="flex items-center gap-2 px-2 py-1.5 border-b border-slate-100 dark:border-white/5 mb-1 sticky top-0 bg-white dark:bg-slate-900 z-10">
<Search className="h-3.5 w-3.5 text-slate-500 shrink-0" />
<input <input
type="text" type="text"
placeholder="Firma suchen..." placeholder="Firma suchen..."
value={companySearch} value={companySearch}
onChange={(e) => setCompanySearch(e.target.value)} onChange={(e) => setCompanySearch(e.target.value)}
className="w-full text-xs bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded px-2 py-1.5 text-slate-900 dark:text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-primary/50" className="w-full text-xs bg-transparent border-0 p-0 focus:outline-none focus:ring-0 text-slate-900 dark:text-slate-200 placeholder-slate-500"
autoFocus onClick={(e) => e.stopPropagation()}
/> />
</div> </div>
<div className="max-h-40 overflow-y-auto">
<button <button
type="button" onClick={() => handleCompanyChange(user.id, '')}
onClick={() => { handleCompanyChange(user.id, ''); setCompanyDropdownOpen(null); setCompanySearch(''); }} className="w-full text-left px-2 py-1.5 text-xs rounded hover:bg-slate-100 dark:hover:bg-white/5 text-red-500 font-medium"
className={`w-full text-left text-xs px-3 py-2 hover:bg-slate-100 dark:hover:bg-white/10 transition-colors ${ >
!user.company_id ? 'text-primary font-semibold' : 'text-slate-700 dark:text-slate-300' Keine Firma (Zuweisung aufheben)
}`} </button>
>
Keine Firma {companies
</button> .filter((c) => c.name.toLowerCase().includes(companySearch.toLowerCase()))
{companies .map((c) => (
.filter((c) => c.name.toLowerCase().includes(companySearch.toLowerCase())) <button
.map((c) => ( key={c.id}
<button onClick={() => handleCompanyChange(user.id, c.id)}
key={c.id} className={`w-full text-left px-2 py-1.5 text-xs rounded hover:bg-slate-100 dark:hover:bg-white/5 transition-colors ${user.company_id === c.id
type="button" ? 'bg-primary/10 text-primary font-medium'
onClick={() => { handleCompanyChange(user.id, c.id); setCompanyDropdownOpen(null); setCompanySearch(''); }} : 'text-slate-700 dark:text-slate-300'
className={`w-full text-left text-xs px-3 py-2 hover:bg-slate-100 dark:hover:bg-white/10 transition-colors ${
user.company_id === c.id ? 'text-primary font-semibold' : 'text-slate-700 dark:text-slate-300'
}`} }`}
> >
{c.name} {c.name}
</button> </button>
))} ))}
</div>
</div> </div>
)} )}
</div> </div>
@@ -367,35 +364,35 @@ export function UserList({ initialUsers, companies }: UserListProps) {
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10" className="h-8 w-8 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10"
title="Passwort zurücksetzen" title="Passwort zurücksetzen"
onClick={() => handleResetPassword(user.email)} onClick={() => handleResetPassword(user.email)}
> >
<Key className="h-4 w-4" /> <Key className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className={role === 'gesperrt' ? 'h-8 w-8 text-green-500 hover:text-green-400 hover:bg-green-500/10' : 'h-8 w-8 text-red-500 hover:text-red-400 hover:bg-red-500/10'} className={role === 'gesperrt' ? 'h-8 w-8 text-green-500 hover:text-green-400 hover:bg-green-500/10' : 'h-8 w-8 text-red-500 hover:text-red-400 hover:bg-red-500/10'}
title={role === 'gesperrt' ? 'Entsperren' : 'Sperren'} title={role === 'gesperrt' ? 'Entsperren' : 'Sperren'}
onClick={() => handleToggleLock(user.id, role)} onClick={() => handleToggleLock(user.id, role)}
disabled={loadingId === user.id + '-lock'} disabled={loadingId === user.id + '-lock'}
> >
{loadingId === user.id + '-lock' ? <Loader2 className="h-4 w-4 animate-spin" /> : role === 'gesperrt' ? <Unlock className="h-4 w-4" /> : <Ban className="h-4 w-4" />} {loadingId === user.id + '-lock' ? <Loader2 className="h-4 w-4 animate-spin" /> : role === 'gesperrt' ? <Unlock className="h-4 w-4" /> : <Ban className="h-4 w-4" />}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10" className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10"
title="Löschen" title="Löschen"
onClick={() => handleDelete(user.id)} onClick={() => handleDelete(user.id)}
disabled={isLoadingDelete} disabled={isLoadingDelete}
> >
{isLoadingDelete ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />} {isLoadingDelete ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>