style: align selection heights in step 1
All checks were successful
Staging Build / build (push) Successful in 2m58s

This commit is contained in:
DanielS
2026-08-10 23:47:50 +02:00
parent 1c9ae2ec31
commit 8b698e42ec

View File

@@ -154,8 +154,28 @@ export function StepCustomer({
Wählen Sie den Partner (für Admins) und den Endkunden aus oder legen Sie einen neuen an. Wählen Sie den Partner (für Admins) und den Endkunden aus oder legen Sie einen neuen an.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> <CardContent className="space-y-6">
<div className={isAdmin ? 'grid grid-cols-1 lg:grid-cols-2 gap-8 items-start' : 'space-y-8'}> {/* Modus-Toggle: Bestandskunde vs. Neuer Kunde */}
<div className="flex gap-2 pb-2">
<Button
variant={customerMode === 'select' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCustomerMode('select')}
className={`gap-2 ${customerMode === 'select' ? '' : 'text-white'}`}
>
<Building2 className="w-4 h-4" /> Bestandskunde wählen
</Button>
<Button
variant={customerMode === 'create' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCustomerMode('create')}
className={`gap-2 ${customerMode === 'create' ? '' : 'text-white'}`}
>
<UserPlus className="w-4 h-4" /> Neuen Kunden anlegen
</Button>
</div>
<div className={isAdmin ? 'grid grid-cols-1 lg:grid-cols-2 gap-8 items-start' : 'space-y-6'}>
{/* ─── 1. ADMIN PARTNER SELECTION ─── */} {/* ─── 1. ADMIN PARTNER SELECTION ─── */}
{isAdmin && ( {isAdmin && (
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 space-y-4"> <div className="p-5 rounded-2xl bg-white/5 border border-white/10 space-y-4">
@@ -349,274 +369,251 @@ export function StepCustomer({
</div> </div>
)} )}
{/* ─── 2. CUSTOMER WRAPPER ─── */} <AnimatePresence mode="wait">
<div className="space-y-6"> {/* ─── 2. CUSTOMER SELECTION ─── */}
{/* Modus-Toggle: Bestandskunde vs. Neuer Kunde */} {customerMode === 'select' && (
<div className="flex gap-2"> <motion.div
<Button key="select"
variant={customerMode === 'select' ? 'default' : 'ghost'} initial={{ opacity: 0, y: 10 }}
size="sm" animate={{ opacity: 1, y: 0 }}
onClick={() => setCustomerMode('select')} exit={{ opacity: 0, y: -10 }}
className={`gap-2 ${customerMode === 'select' ? '' : 'text-white'}`} transition={{ duration: 0.2 }}
className="p-5 rounded-2xl bg-white/5 border border-white/10 space-y-4"
> >
<Building2 className="w-4 h-4" /> Bestandskunde wählen <div className="flex items-center justify-between gap-4 flex-wrap">
</Button> <div>
<Button <Label className="text-white font-bold text-base flex items-center gap-2">
variant={customerMode === 'create' ? 'default' : 'ghost'} <User className="w-5 h-5 text-primary" />
size="sm" Kunden-Auswahl
onClick={() => setCustomerMode('create')} </Label>
className={`gap-2 ${customerMode === 'create' ? '' : 'text-white'}`} <p className="text-xs text-slate-400 mt-0.5">
> {isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
<UserPlus className="w-4 h-4" /> Neuen Kunden anlegen ? `Endkunden für den Partner "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"`
</Button> : 'Alle verfügbaren Endkunden'}
</div> </p>
<AnimatePresence mode="wait">
{/* ─── 2. CUSTOMER SELECTION ─── */}
{customerMode === 'select' && (
<motion.div
key="select"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="p-5 rounded-2xl bg-white/5 border border-white/10 space-y-4"
>
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<Label className="text-white font-bold text-base flex items-center gap-2">
<User className="w-5 h-5 text-primary" />
Kunden-Auswahl
</Label>
<p className="text-xs text-slate-400 mt-0.5">
{isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
? `Endkunden für den Partner "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"`
: 'Alle verfügbaren Endkunden'}
</p>
</div>
</div> </div>
</div>
{customersByCompany.length === 0 ? ( {customersByCompany.length === 0 ? (
<div className="p-6 rounded-xl bg-white/5 border border-white/10 text-center space-y-3"> <div className="p-6 rounded-xl bg-white/5 border border-white/10 text-center space-y-3">
<Building2 className="w-10 h-10 text-slate-600 mx-auto" /> <Building2 className="w-10 h-10 text-slate-600 mx-auto" />
<p className="text-slate-400"> <p className="text-slate-400">
{isAdmin && selectedCompanyId && selectedCompanyId !== 'all' {isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
? 'Keine Endkunden für den ausgewählten Partner vorhanden.' ? 'Keine Endkunden für den ausgewählten Partner vorhanden.'
: 'Noch keine Endkunden vorhanden.'} : 'Noch keine Endkunden vorhanden.'}
</p> </p>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCustomerMode('create')} onClick={() => setCustomerMode('create')}
className="border-white/10 text-white" className="border-white/10 text-white"
> >
<UserPlus className="w-4 h-4 mr-2" /> Ersten Kunden anlegen <UserPlus className="w-4 h-4 mr-2" /> Ersten Kunden anlegen
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{/* Customer Header Controls: Search & Page Size */} {/* Customer Header Controls: Search & Page Size */}
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between"> <div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input <Input
placeholder="Kunden suchen nach Name, Ort, PLZ..." placeholder="Kunden suchen nach Name, Ort, PLZ..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-sm h-9" className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-sm h-9"
/> />
</div>
{/* Page Size Selector */}
<div className="flex items-center gap-2 self-end sm:self-auto">
<span className="text-xs text-slate-400 whitespace-nowrap">Kunden pro Seite:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="h-9 rounded-md border border-white/10 bg-slate-900 px-2 py-1 text-xs text-white shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div> </div>
{filteredCustomers.length === 0 ? ( {/* Page Size Selector */}
<div className="p-4 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5"> <div className="flex items-center gap-2 self-end sm:self-auto">
Keine passenden Kunden gefunden. <span className="text-xs text-slate-400 whitespace-nowrap">Kunden pro Seite:</span>
</div> <select
) : ( value={pageSize}
<> onChange={(e) => setPageSize(Number(e.target.value))}
{/* Customer Card List */} className="h-9 rounded-md border border-white/10 bg-slate-900 px-2 py-1 text-xs text-white shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
<div className="space-y-2 max-h-[30rem] overflow-y-auto pr-1"> >
{paginatedCustomers.map((customer) => { <option value={10}>10</option>
const isSelected = selectedEndCustomerId === customer.id <option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div>
return ( {filteredCustomers.length === 0 ? (
<div <div className="p-4 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5">
key={customer.id} Keine passenden Kunden gefunden.
onClick={() => handleSelectCustomer(customer)} </div>
className={`flex items-start p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ ) : (
isSelected <>
? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' {/* Customer Card List */}
: 'border-white/5 bg-white/5 hover:bg-white/10' <div className="space-y-2 max-h-[30rem] overflow-y-auto pr-1">
}`} {paginatedCustomers.map((customer) => {
> const isSelected = selectedEndCustomerId === customer.id
<input
type="radio" return (
name="end_customer" <div
value={customer.id} key={customer.id}
checked={isSelected} onClick={() => handleSelectCustomer(customer)}
onChange={() => handleSelectCustomer(customer)} className={`flex items-start p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${
className="sr-only" isSelected
/> ? 'border-primary bg-primary/10 shadow-lg shadow-primary/5'
<div className="flex-1"> : 'border-white/5 bg-white/5 hover:bg-white/10'
<div className="flex items-center gap-2 flex-wrap"> }`}
<p className="font-semibold text-white text-sm">{customer.company_name}</p> >
{isAdmin && customer.partner_id && ( <input
<Badge type="radio"
variant="outline" name="end_customer"
className="text-[10px] border-white/10 text-slate-400 bg-white/5" value={customer.id}
> checked={isSelected}
Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id} onChange={() => handleSelectCustomer(customer)}
</Badge> className="sr-only"
)} />
</div> <div className="flex-1">
<p className="text-xs text-slate-400 mt-0.5"> <div className="flex items-center gap-2 flex-wrap">
{[customer.first_name, customer.last_name].filter(Boolean).join(' ')} <p className="font-semibold text-white text-sm">{customer.company_name}</p>
{customer.first_name || customer.last_name ? ' · ' : ''} {isAdmin && customer.partner_id && (
{[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')} <Badge
</p> variant="outline"
className="text-[10px] border-white/10 text-slate-400 bg-white/5"
>
Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id}
</Badge>
)}
</div> </div>
{isSelected && <Check className="w-5 h-5 text-primary mt-0.5 shrink-0" />} <p className="text-xs text-slate-400 mt-0.5">
{[customer.first_name, customer.last_name].filter(Boolean).join(' ')}
{customer.first_name || customer.last_name ? ' · ' : ''}
{[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')}
</p>
</div> </div>
); {isSelected && <Check className="w-5 h-5 text-primary mt-0.5 shrink-0" />}
})} </div>
);
})}
</div>
{/* Customer Pagination Footer */}
<div className="flex items-center justify-between border-t border-white/10 pt-3 text-xs text-slate-400 flex-wrap gap-2">
<div>
Zeige {totalCustomerCount === 0 ? 0 : (currentPage - 1) * pageSize + 1} bis{' '}
{Math.min(currentPage * pageSize, totalCustomerCount)} von {totalCustomerCount} Kunden
</div> </div>
{/* Customer Pagination Footer */} <div className="flex items-center gap-1">
<div className="flex items-center justify-between border-t border-white/10 pt-3 text-xs text-slate-400 flex-wrap gap-2"> <Button
<div> variant="ghost"
Zeige {totalCustomerCount === 0 ? 0 : (currentPage - 1) * pageSize + 1} bis{' '} size="sm"
{Math.min(currentPage * pageSize, totalCustomerCount)} von {totalCustomerCount} Kunden onClick={() => setCurrentPage(1)}
</div> disabled={currentPage === 1}
className="h-8 px-2 text-slate-400 hover:text-white"
title="Erste Seite"
>
<ChevronsLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="h-8 px-2.5 border-white/10 text-white"
>
<ChevronLeft className="w-3.5 h-3.5 mr-1" /> Zurück
</Button>
<div className="flex items-center gap-1"> <span className="px-2 font-medium text-slate-300">
<Button Seite {currentPage} von {totalCustomerPages}
variant="ghost" </span>
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="h-8 px-2 text-slate-400 hover:text-white"
title="Erste Seite"
>
<ChevronsLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="h-8 px-2.5 border-white/10 text-white"
>
<ChevronLeft className="w-3.5 h-3.5 mr-1" /> Zurück
</Button>
<span className="px-2 font-medium text-slate-300"> <Button
Seite {currentPage} von {totalCustomerPages} variant="outline"
</span> size="sm"
onClick={() => setCurrentPage((p) => Math.min(totalCustomerPages, p + 1))}
<Button disabled={currentPage === totalCustomerPages}
variant="outline" className="h-8 px-2.5 border-white/10 text-white"
size="sm" >
onClick={() => setCurrentPage((p) => Math.min(totalCustomerPages, p + 1))} Vor <ChevronRight className="w-3.5 h-3.5 ml-1" />
disabled={currentPage === totalCustomerPages} </Button>
className="h-8 px-2.5 border-white/10 text-white" <Button
> variant="ghost"
Vor <ChevronRight className="w-3.5 h-3.5 ml-1" /> size="sm"
</Button> onClick={() => setCurrentPage(totalCustomerPages)}
<Button disabled={currentPage === totalCustomerPages}
variant="ghost" className="h-8 px-2 text-slate-400 hover:text-white"
size="sm" title="Letzte Seite"
onClick={() => setCurrentPage(totalCustomerPages)} >
disabled={currentPage === totalCustomerPages} <ChevronsRight className="w-4 h-4" />
className="h-8 px-2 text-slate-400 hover:text-white" </Button>
title="Letzte Seite"
>
<ChevronsRight className="w-4 h-4" />
</Button>
</div>
</div> </div>
</> </div>
)} </>
</div> )}
)}
</motion.div>
)}
{/* ─── MODUS B: NEUEN KUNDEN ANLEGEN ─── */}
{customerMode === 'create' && (
<motion.div
key="create"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-xl bg-white/5 border border-white/10"
>
{(
[
{ label: 'Firmenname *', key: 'company_name', span: true, placeholder: 'GmbH / Einzelunternehmen' },
{ label: 'USt-IdNr.', key: 'vat_id', span: false, placeholder: 'DE123456789' },
{ label: 'Vorname', key: 'first_name', span: false, placeholder: '' },
{ label: 'Nachname', key: 'last_name', span: false, placeholder: '' },
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' },
{ label: 'PLZ', key: 'zip', span: false, placeholder: '' },
{ label: 'Ort', key: 'city', span: false, placeholder: '' },
{ label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' },
{ label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' },
{ label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' },
{ label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' },
] as const
).map(({ label, key, span, placeholder }) => (
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
<Label className="text-slate-300 text-sm">{label}</Label>
<Input
value={newCustomerForm[key] || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, [key]: e.target.value }))
}
placeholder={placeholder}
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500"
/>
</div>
))}
<div className="md:col-span-2 flex gap-2">
<Button
onClick={handleCreateCustomer}
disabled={isCreatingCustomer || !newCustomerForm.company_name?.trim()}
className="gap-2"
>
{isCreatingCustomer ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<UserPlus className="w-4 h-4" />
)}
Kunden speichern & auswählen
</Button>
<Button
variant="ghost"
className="text-white"
onClick={() => setCustomerMode('select')}
>
Abbrechen
</Button>
</div> </div>
</motion.div> )}
)} </motion.div>
</AnimatePresence> )}
</div>
{/* ─── MODUS B: NEUEN KUNDEN ANLEGEN ─── */}
{customerMode === 'create' && (
<motion.div
key="create"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-xl bg-white/5 border border-white/10"
>
{(
[
{ label: 'Firmenname *', key: 'company_name', span: true, placeholder: 'GmbH / Einzelunternehmen' },
{ label: 'USt-IdNr.', key: 'vat_id', span: false, placeholder: 'DE123456789' },
{ label: 'Vorname', key: 'first_name', span: false, placeholder: '' },
{ label: 'Nachname', key: 'last_name', span: false, placeholder: '' },
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' },
{ label: 'PLZ', key: 'zip', span: false, placeholder: '' },
{ label: 'Ort', key: 'city', span: false, placeholder: '' },
{ label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' },
{ label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' },
{ label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' },
{ label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' },
] as const
).map(({ label, key, span, placeholder }) => (
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
<Label className="text-slate-300 text-sm">{label}</Label>
<Input
value={newCustomerForm[key] || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, [key]: e.target.value }))
}
placeholder={placeholder}
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500"
/>
</div>
))}
<div className="md:col-span-2 flex gap-2">
<Button
onClick={handleCreateCustomer}
disabled={isCreatingCustomer || !newCustomerForm.company_name?.trim()}
className="gap-2"
>
{isCreatingCustomer ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<UserPlus className="w-4 h-4" />
)}
Kunden speichern & auswählen
</Button>
<Button
variant="ghost"
className="text-white"
onClick={() => setCustomerMode('select')}
>
Abbrechen
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex justify-end border-t border-white/10 pt-6"> <CardFooter className="flex justify-end border-t border-white/10 pt-6">