feat: Add partner account settings page under /account-kunden
All checks were successful
Staging Build / build (push) Successful in 3m33s

This commit is contained in:
DanielS
2026-07-04 21:06:46 +02:00
parent 060aa97ce5
commit a64b6790c6
3 changed files with 523 additions and 0 deletions

View File

@@ -0,0 +1,346 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { updateUserProfile, updateCompanyDetails } from '@/lib/actions/users';
import { updatePassword } from '@/lib/actions/auth';
import { Save, KeyRound, Loader2 } from 'lucide-react';
interface AccountSettingsFormProps {
userId: string;
profile: any;
company: any;
}
export function AccountSettingsForm({ userId, profile, company }: AccountSettingsFormProps) {
// Profil State
const [firstName, setFirstName] = useState(profile?.first_name || '');
const [lastName, setLastName] = useState(profile?.last_name || '');
const [profileMessage, setProfileMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [isUpdatingProfile, setIsUpdatingProfile] = useState(false);
// Company State
const [street, setStreet] = useState(company?.street || '');
const [zip, setZip] = useState(company?.zip || '');
const [city, setCity] = useState(company?.city || '');
const [companyEmail, setCompanyEmail] = useState(company?.email || '');
const [companyMessage, setCompanyMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [isUpdatingCompany, setIsUpdatingCompany] = useState(false);
// Passwort State
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordMessage, setPasswordMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [isUpdatingPassword, setIsUpdatingPassword] = useState(false);
// Profil speichern
const handleUpdateProfile = async (e: React.FormEvent) => {
e.preventDefault();
setProfileMessage(null);
setIsUpdatingProfile(true);
try {
const res = await updateUserProfile(userId, {
first_name: firstName || null,
last_name: lastName || null,
});
if (res.success) {
setProfileMessage({ type: 'success', text: 'Persönliche Daten erfolgreich aktualisiert.' });
} else {
setProfileMessage({ type: 'error', text: res.error || 'Fehler beim Aktualisieren des Profils.' });
}
} catch (err: any) {
setProfileMessage({ type: 'error', text: err.message || 'Ein unerwarteter Fehler ist aufgetreten.' });
} finally {
setIsUpdatingProfile(false);
}
};
// Company speichern
const handleUpdateCompany = async (e: React.FormEvent) => {
e.preventDefault();
if (!company?.id) return;
setCompanyMessage(null);
setIsUpdatingCompany(true);
try {
const res = await updateCompanyDetails(company.id, {
street: street || null,
zip: zip || null,
city: city || null,
email: companyEmail || null,
});
if (res.success) {
setCompanyMessage({ type: 'success', text: 'Unternehmensdaten erfolgreich aktualisiert.' });
} else {
setCompanyMessage({ type: 'error', text: res.error || 'Fehler beim Aktualisieren der Unternehmensdaten.' });
}
} catch (err: any) {
setCompanyMessage({ type: 'error', text: err.message || 'Ein unerwarteter Fehler ist aufgetreten.' });
} finally {
setIsUpdatingCompany(false);
}
};
// Passwort speichern
const handleUpdatePassword = async (e: React.FormEvent) => {
e.preventDefault();
setPasswordMessage(null);
if (newPassword.length < 6) {
setPasswordMessage({ type: 'error', text: 'Das Passwort muss mindestens 6 Zeichen lang sein.' });
return;
}
if (newPassword !== confirmPassword) {
setPasswordMessage({ type: 'error', text: 'Die Passwörter stimmen nicht überein.' });
return;
}
setIsUpdatingPassword(true);
try {
const res = await updatePassword(newPassword);
if (res.success) {
setPasswordMessage({ type: 'success', text: 'Passwort erfolgreich geändert.' });
setNewPassword('');
setConfirmPassword('');
} else {
setPasswordMessage({ type: 'error', text: res.error || 'Fehler beim Ändern des Passworts.' });
}
} catch (err: any) {
setPasswordMessage({ type: 'error', text: err.message || 'Ein unerwarteter Fehler ist aufgetreten.' });
} finally {
setIsUpdatingPassword(false);
}
};
return (
<div className="space-y-8">
{/* 1. Persönliche Daten */}
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-xl">Persönliche Daten</CardTitle>
<CardDescription className="text-slate-400">
Aktualisieren Sie Ihren Vor- und Nachnamen.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpdateProfile} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Vorname</Label>
<Input
id="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="Max"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nachname</Label>
<Input
id="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="Mustermann"
/>
</div>
</div>
{profileMessage && (
<div
className={`p-3 rounded-lg text-sm border ${
profileMessage.type === 'success'
? 'bg-green-500/10 border-green-500/20 text-green-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'
}`}
>
{profileMessage.text}
</div>
)}
<div className="flex justify-end">
<Button type="submit" disabled={isUpdatingProfile} className="gap-2">
{isUpdatingProfile ? (
<>
<Loader2 className="w-4 h-4 animate-spin" /> Speichern...
</>
) : (
<>
<Save className="w-4 h-4" /> Profil speichern
</>
)}
</Button>
</div>
</form>
</CardContent>
</Card>
{/* 2. Unternehmensdaten */}
{company && (
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-xl">Unternehmensdaten</CardTitle>
<CardDescription className="text-slate-400">
Verwalten Sie die Adressdaten Ihres Partner-Unternehmens.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpdateCompany} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="companyName">Firmenname (nicht änderbar)</Label>
<Input
id="companyName"
value={company.name}
disabled
className="bg-slate-950/50 border-white/5 text-slate-400 cursor-not-allowed"
/>
</div>
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="street">Straße & Hausnummer</Label>
<Input
id="street"
value={street}
onChange={(e) => setStreet(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="Musterstraße 12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="zip">Postleitzahl</Label>
<Input
id="zip"
value={zip}
onChange={(e) => setZip(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="12345"
/>
</div>
<div className="space-y-2">
<Label htmlFor="city">Ort</Label>
<Input
id="city"
value={city}
onChange={(e) => setCity(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="Musterstadt"
/>
</div>
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="companyEmail">E-Mail für Rechnungen / Anfragen</Label>
<Input
id="companyEmail"
type="email"
value={companyEmail}
onChange={(e) => setCompanyEmail(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="partner@firma.de"
/>
</div>
</div>
{companyMessage && (
<div
className={`p-3 rounded-lg text-sm border ${
companyMessage.type === 'success'
? 'bg-green-500/10 border-green-500/20 text-green-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'
}`}
>
{companyMessage.text}
</div>
)}
<div className="flex justify-end">
<Button type="submit" disabled={isUpdatingCompany} className="gap-2">
{isUpdatingCompany ? (
<>
<Loader2 className="w-4 h-4 animate-spin" /> Speichern...
</>
) : (
<>
<Save className="w-4 h-4" /> Firmendaten speichern
</>
)}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* 3. Sicherheit / Passwort ändern */}
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-xl">Passwort ändern</CardTitle>
<CardDescription className="text-slate-400">
Geben Sie ein neues Passwort für Ihren Zugang ein.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpdatePassword} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="newPassword">Neues Passwort</Label>
<Input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="••••••••"
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Passwort bestätigen</Label>
<Input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="bg-slate-950 border-white/10 focus:border-primary text-white"
placeholder="••••••••"
/>
</div>
</div>
{passwordMessage && (
<div
className={`p-3 rounded-lg text-sm border ${
passwordMessage.type === 'success'
? 'bg-green-500/10 border-green-500/20 text-green-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'
}`}
>
{passwordMessage.text}
</div>
)}
<div className="flex justify-end">
<Button type="submit" disabled={isUpdatingPassword} className="gap-2">
{isUpdatingPassword ? (
<>
<Loader2 className="w-4 h-4 animate-spin" /> Ändern...
</>
) : (
<>
<KeyRound className="w-4 h-4" /> Passwort aktualisieren
</>
)}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,131 @@
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, User as UserIcon, Building2, ShieldAlert } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { AccountSettingsForm } from './AccountSettingsForm';
export default async function AccountKundenPage() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/auth/login?next=/account-kunden');
}
// 1. Hole User aus custom users Tabelle
const { data: dbUser } = await supabase
.from('users')
.select('role, company_id')
.eq('id', user.id)
.single();
// 2. Hole Profil-Daten
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
// 3. Hole Company-Daten falls vorhanden
let company = null;
if (dbUser?.company_id) {
const { data: companyData } = await supabase
.from('companies')
.select('*')
.eq('id', dbUser.company_id)
.single();
company = companyData;
}
return (
<div className="min-h-screen bg-[#020617] text-white px-4 py-12">
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center gap-4">
<Link href="/">
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-white">
<ArrowLeft className="w-4 h-4 mr-2" /> Startseite
</Button>
</Link>
<div>
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
<UserIcon className="w-8 h-8 text-primary" />
Account Einstellungen
</h1>
<p className="text-slate-400 text-sm mt-1">
Verwalten Sie Ihre persönlichen Profildaten, Unternehmensdaten und Sicherheitseinstellungen.
</p>
</div>
</div>
{/* Dashboard Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Linke Infospalte */}
<div className="space-y-6">
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<UserIcon className="w-5 h-5 text-primary" />
Benutzer-Info
</CardTitle>
<CardDescription className="text-slate-400 text-xs">
Ihr aktueller Systemstatus
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div>
<span className="text-slate-500 block text-xs uppercase tracking-wider">E-Mail-Adresse</span>
<span className="text-white font-medium">{user.email}</span>
</div>
<div>
<span className="text-slate-500 block text-xs uppercase tracking-wider">Rolle</span>
<span className="text-amber-400 font-bold uppercase text-xs tracking-wider bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
{dbUser?.role || 'partner'}
</span>
</div>
{company && (
<div>
<span className="text-slate-500 block text-xs uppercase tracking-wider">Unternehmen</span>
<span className="text-white font-medium flex items-center gap-1.5 mt-1">
<Building2 className="w-4 h-4 text-slate-400" />
{company.name}
</span>
</div>
)}
</CardContent>
</Card>
{!dbUser?.company_id && (
<Card className="border-red-500/20 bg-red-500/5">
<CardContent className="p-4 flex gap-3 text-red-400 text-sm">
<ShieldAlert className="w-5 h-5 shrink-0 mt-0.5" />
<div>
<p className="font-bold">Kein Unternehmen zugewiesen</p>
<p className="text-xs text-red-400/80 mt-1">
Wenden Sie sich an einen Administrator, um Ihrem Account ein Partner-Unternehmen zuzuweisen.
</p>
</div>
</CardContent>
</Card>
)}
</div>
{/* Rechte Formularspalte */}
<div className="md:col-span-2">
<AccountSettingsForm
userId={user.id}
profile={profile}
company={company}
/>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
'use server'; 'use server';
import { createAdminClient } from '@/lib/supabase/admin'; import { createAdminClient } from '@/lib/supabase/admin';
import { createClient } from '@/lib/supabase/server';
import { revalidatePath, unstable_noStore as noStore } from 'next/cache'; import { revalidatePath, unstable_noStore as noStore } from 'next/cache';
import { sendMail } from '@/utils/mail'; import { sendMail } from '@/utils/mail';
@@ -262,6 +263,51 @@ export async function updateUserProfile(id: string, data: { first_name: string |
} }
} }
export async function updateCompanyDetails(companyId: string, data: { street: string | null; zip: string | null; city: string | null; email: string | null }) {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht authentifiziert.' };
}
// Berechtigung prüfen: Ist der User Mitglied dieser Company oder Admin?
const { data: dbUser } = await supabase
.from('users')
.select('role, company_id')
.eq('id', user.id)
.single();
if (!dbUser) {
return { success: false, error: 'Benutzer nicht gefunden.' };
}
if (dbUser.role !== 'admin' && dbUser.company_id !== companyId) {
return { success: false, error: 'Keine Berechtigung zur Bearbeitung dieses Unternehmens.' };
}
const admin = createAdminClient();
const { error } = await admin
.from('companies')
.update({
street: data.street,
zip: data.zip,
city: data.city,
email: data.email
})
.eq('id', companyId);
if (error) {
return { success: false, error: error.message };
}
return { success: true };
} catch (err: any) {
console.error('updateCompanyDetails Server Action failed:', err);
return { success: false, error: err.message || 'Unternehmen konnte nicht aktualisiert werden.' };
}
}
function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) { function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) {
return ` return `
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f8fafc; padding: 40px 10px; margin: 0; width: 100%;"> <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f8fafc; padding: 40px 10px; margin: 0; width: 100%;">