mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:41:16 +00:00
added personal settings for all users
This commit is contained in:
parent
ddd7a41384
commit
b882094601
8 changed files with 708 additions and 8 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -41,4 +41,6 @@ yarn-error.log*
|
|||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated/prisma
|
||||
/src/generated/prisma
|
||||
|
||||
/graphify-out/*
|
||||
31
src/app/auth/callback/route.ts
Normal file
31
src/app/auth/callback/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { syncEmailAfterConfirmation } from "@/src/modules/settings/actions/personal-settings-actions";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const code = searchParams.get("code");
|
||||
const next = searchParams.get("next") ?? "/settings/personal";
|
||||
const isEmailChange = searchParams.get("email_confirmed") === "true";
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.redirect(new URL(next, request.url));
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.redirect(new URL(next, request.url));
|
||||
}
|
||||
|
||||
if (isEmailChange) {
|
||||
await syncEmailAfterConfirmation();
|
||||
}
|
||||
|
||||
// Hard redirect so the browser re-fetches the page fully,
|
||||
// bypassing any Next.js client-side cache.
|
||||
const url = new URL(next, request.url);
|
||||
url.searchParams.set("t", Date.now().toString());
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
3
src/app/auth/layout.tsx
Normal file
3
src/app/auth/layout.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ export type UserData = {
|
|||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
tel: string | null;
|
||||
birthday: Date | null;
|
||||
plz: string | null;
|
||||
city: string | null;
|
||||
|
|
@ -83,6 +84,7 @@ export const getUserData = cache(async (): Promise<AuthUserData | null> => {
|
|||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
tel: true,
|
||||
birthday: true,
|
||||
plz: true,
|
||||
city: true,
|
||||
|
|
|
|||
236
src/modules/settings/actions/personal-settings-actions.ts
Normal file
236
src/modules/settings/actions/personal-settings-actions.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"use server";
|
||||
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { createAdminClient } from "@/src/lib/supabase/admin";
|
||||
import { requireAuthWithData, isManager, isEmployee, isUser } from "@/src/lib/auth/getUser";
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
type ActionResult = { success: boolean; error?: string };
|
||||
|
||||
function revalidateUserCache(email: string) {
|
||||
(revalidateTag as (tag: string, type: "max") => void)(`user-data-${email}`, "max");
|
||||
}
|
||||
|
||||
export async function updateProfileInfo(data: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
name?: string;
|
||||
tel?: string;
|
||||
gender?: string;
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
if (isManager(userData)) {
|
||||
await prisma.manager.update({
|
||||
where: { id: userData.id },
|
||||
data: {
|
||||
firstName: data.firstName ?? userData.firstName,
|
||||
lastName: data.lastName ?? userData.lastName,
|
||||
tel: data.tel ?? userData.tel,
|
||||
},
|
||||
});
|
||||
} else if (isEmployee(userData)) {
|
||||
await prisma.employee.update({
|
||||
where: { id: userData.id },
|
||||
data: {
|
||||
firstName: data.firstName ?? userData.firstName,
|
||||
lastName: data.lastName ?? userData.lastName,
|
||||
},
|
||||
});
|
||||
} else if (isUser(userData)) {
|
||||
await prisma.user.update({
|
||||
where: { id: userData.id },
|
||||
data: {
|
||||
name: data.name ?? userData.name,
|
||||
tel: data.tel,
|
||||
gender: data.gender,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
revalidateUserCache(userData.email);
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Aktualisieren der Profildaten." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAddress(data: {
|
||||
street: string;
|
||||
plz: string;
|
||||
city: string;
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
if (!isUser(userData)) {
|
||||
return { success: false, error: "Adresse kann nur von regulären Nutzern geändert werden." };
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userData.id },
|
||||
data: {
|
||||
street: data.street,
|
||||
plz: data.plz,
|
||||
city: data.city,
|
||||
},
|
||||
});
|
||||
|
||||
revalidateUserCache(userData.email);
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Aktualisieren der Adresse." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateEmail(newEmail: string): Promise<ActionResult> {
|
||||
try {
|
||||
await requireAuthWithData();
|
||||
|
||||
if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
|
||||
return { success: false, error: "Ungültige E-Mail-Adresse." };
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
||||
const supabase = await createClient();
|
||||
const { error: authError } = await supabase.auth.updateUser(
|
||||
{ email: newEmail },
|
||||
{ emailRedirectTo: `${baseUrl}/auth/callback?next=/settings/personal&email_confirmed=true` }
|
||||
);
|
||||
|
||||
if (authError) {
|
||||
return { success: false, error: authError.message };
|
||||
}
|
||||
|
||||
// Prisma is updated in /auth/callback after the user confirms the link.
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Aktualisieren der E-Mail." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncEmailAfterConfirmation(): Promise<ActionResult> {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (error || !user?.email) {
|
||||
return { success: false, error: "Nicht authentifiziert." };
|
||||
}
|
||||
|
||||
const { id, email: newEmail } = user;
|
||||
|
||||
const prismaUser = await prisma.user.findUnique({ where: { id }, select: { id: true } });
|
||||
if (prismaUser) {
|
||||
await prisma.user.update({ where: { id }, data: { email: newEmail } });
|
||||
} else {
|
||||
const manager = await prisma.manager.findUnique({ where: { id }, select: { id: true } });
|
||||
if (manager) {
|
||||
await prisma.manager.update({ where: { id }, data: { email: newEmail } });
|
||||
} else {
|
||||
await prisma.employee.updateMany({ where: { id }, data: { email: newEmail } });
|
||||
}
|
||||
}
|
||||
|
||||
revalidateUserCache(newEmail);
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Synchronisieren der E-Mail." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePassword(
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
if (newPassword.length < 8) {
|
||||
return { success: false, error: "Das neue Passwort muss mindestens 8 Zeichen lang sein." };
|
||||
}
|
||||
|
||||
const userData = await requireAuthWithData();
|
||||
const supabase = await createClient();
|
||||
|
||||
// Re-authenticate to verify current password
|
||||
const { error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email: userData.email,
|
||||
password: currentPassword,
|
||||
});
|
||||
|
||||
if (signInError) {
|
||||
return { success: false, error: "Das aktuelle Passwort ist falsch." };
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({ password: newPassword });
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Aktualisieren des Passworts." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAvatar(formData: FormData): Promise<ActionResult & { url?: string }> {
|
||||
try {
|
||||
const userData = await requireAuthWithData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
return { success: false, error: "Keine Datei übergeben." };
|
||||
}
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return { success: false, error: "Nur Bilddateien erlaubt." };
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return { success: false, error: "Datei darf maximal 5 MB groß sein." };
|
||||
}
|
||||
|
||||
const extension =
|
||||
file.name
|
||||
.split(".")
|
||||
.pop()
|
||||
?.replace(/[^a-zA-Z0-9]/g, "") || "jpg";
|
||||
|
||||
const folder = isManager(userData) ? "managers" : isEmployee(userData) ? "employees" : "users";
|
||||
const safePath = `${folder}/${userData.id}/${Date.now()}.${extension}`;
|
||||
|
||||
const adminClient = createAdminClient();
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const { data, error } = await adminClient.storage.from("avatars").upload(safePath, buffer, {
|
||||
contentType: file.type,
|
||||
upsert: true,
|
||||
cacheControl: "3600",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = adminClient.storage.from("avatars").getPublicUrl(data.path);
|
||||
|
||||
if (isManager(userData)) {
|
||||
await prisma.manager.update({ where: { id: userData.id }, data: { pbSrc: publicUrl } });
|
||||
} else if (isEmployee(userData)) {
|
||||
await prisma.employee.update({ where: { id: userData.id }, data: { pbSrc: publicUrl } });
|
||||
} else if (isUser(userData)) {
|
||||
await prisma.user.update({ where: { id: userData.id }, data: { pbSrc: publicUrl } });
|
||||
}
|
||||
|
||||
revalidateUserCache(userData.email);
|
||||
return { success: true, url: publicUrl };
|
||||
} catch {
|
||||
return { success: false, error: "Fehler beim Hochladen des Profilbilds." };
|
||||
}
|
||||
}
|
||||
361
src/modules/settings/ui/components/PersonalSettingsForm.tsx
Normal file
361
src/modules/settings/ui/components/PersonalSettingsForm.tsx
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import ImageUploadComponent from "@/src/components/layout/ImageUploadComponent";
|
||||
import InputComponent from "@/src/components/layout/InputComponent";
|
||||
import DropdownComponent from "@/src/components/layout/DropdownComponent";
|
||||
import AddressAutocompleteComponent, {
|
||||
ParsedAddress,
|
||||
} from "@/src/components/layout/AddressAutocompleteComponent";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
updateProfileInfo,
|
||||
updateAddress,
|
||||
updateEmail,
|
||||
updatePassword,
|
||||
updateAvatar,
|
||||
} from "../../actions/personal-settings-actions";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type PersonalSettingsProps = {
|
||||
role: "user" | "manager" | "employee";
|
||||
id: string;
|
||||
email: string;
|
||||
pbSrc: string | null;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
tel?: string | null;
|
||||
name?: string | null;
|
||||
gender?: string | null;
|
||||
street?: string | null;
|
||||
plz?: string | null;
|
||||
city?: string | null;
|
||||
};
|
||||
|
||||
// ── Shared card shell ────────────────────────────────────────────────────────
|
||||
function Card({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-6 space-y-5">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-gray-900">{title}</h2>
|
||||
{subtitle && <p className="text-xs text-gray-400 mt-0.5">{subtitle}</p>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main component ───────────────────────────────────────────────────────────
|
||||
const PersonalSettingsForm = ({
|
||||
role,
|
||||
email: initialEmail,
|
||||
pbSrc,
|
||||
firstName: initialFirstName,
|
||||
lastName: initialLastName,
|
||||
tel: initialTel,
|
||||
name: initialName,
|
||||
gender: initialGender,
|
||||
street: initialStreet,
|
||||
plz: initialPlz,
|
||||
city: initialCity,
|
||||
}: PersonalSettingsProps) => {
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
// Avatar
|
||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(pbSrc);
|
||||
|
||||
// Profile info
|
||||
const [firstName, setFirstName] = useState(initialFirstName ?? "");
|
||||
const [lastName, setLastName] = useState(initialLastName ?? "");
|
||||
const [name, setName] = useState(initialName ?? "");
|
||||
const [tel, setTel] = useState(initialTel ?? "");
|
||||
const [gender, setGender] = useState(initialGender ?? "");
|
||||
|
||||
// Address
|
||||
const [street, setStreet] = useState(initialStreet ?? "");
|
||||
const [plz, setPlz] = useState(initialPlz ?? "");
|
||||
const [city, setCity] = useState(initialCity ?? "");
|
||||
|
||||
// Email
|
||||
const [email, setEmail] = useState(initialEmail);
|
||||
|
||||
// Password
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────
|
||||
|
||||
const handleAvatarSave = () => {
|
||||
if (!avatarFile) return;
|
||||
startTransition(async () => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", avatarFile);
|
||||
const result = await updateAvatar(fd);
|
||||
if (result.success) {
|
||||
toast.success("Profilbild erfolgreich gespeichert.");
|
||||
setAvatarFile(null);
|
||||
} else {
|
||||
toast.error(result.error ?? "Fehler beim Hochladen.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSave = () => {
|
||||
startTransition(async () => {
|
||||
const result = await updateProfileInfo({ firstName, lastName, name, tel, gender });
|
||||
if (result.success) {
|
||||
toast.success("Profildaten erfolgreich gespeichert.");
|
||||
} else {
|
||||
toast.error(result.error ?? "Fehler beim Speichern.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressSave = () => {
|
||||
startTransition(async () => {
|
||||
const result = await updateAddress({ street, plz, city });
|
||||
if (result.success) {
|
||||
toast.success("Adresse erfolgreich gespeichert.");
|
||||
} else {
|
||||
toast.error(result.error ?? "Fehler beim Speichern.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEmailSave = () => {
|
||||
startTransition(async () => {
|
||||
const result = await updateEmail(email);
|
||||
if (result.success) {
|
||||
toast.success("Bestätigungslink wurde an deine neue E-Mail-Adresse gesendet.");
|
||||
} else {
|
||||
toast.error(result.error ?? "Fehler beim Speichern.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordSave = () => {
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error("Die Passwörter stimmen nicht überein.");
|
||||
return;
|
||||
}
|
||||
startTransition(async () => {
|
||||
const result = await updatePassword(currentPassword, newPassword);
|
||||
if (result.success) {
|
||||
toast.success("Passwort erfolgreich geändert.");
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
} else {
|
||||
toast.error(result.error ?? "Fehler beim Speichern.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ── Hero: Profilbild ─────────────────────────────────────────────
|
||||
Full-width card with centred avatar, accent gradient like dashboard */}
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-8 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_100%_0%,oklch(0.88_0.09_85/_0.25)_0%,transparent_70%)] pointer-events-none" />
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-xs font-semibold tracking-widest text-gray-400 uppercase">
|
||||
Profilbild
|
||||
</p>
|
||||
<ImageUploadComponent
|
||||
value={avatarFile}
|
||||
preview={avatarPreview}
|
||||
onChange={(file, preview) => {
|
||||
setAvatarFile(file);
|
||||
setAvatarPreview(preview);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAvatarSave}
|
||||
disabled={!avatarFile || pending}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/60 border-white/60 backdrop-blur-sm"
|
||||
>
|
||||
Profilbild speichern
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 2-column grid on lg+, single column on mobile ─────────────── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* ── Persönliche Daten ───────────────────────────────────────── */}
|
||||
<Card title="Persönliche Daten" subtitle="Vor- und Nachname, Telefon, Geschlecht">
|
||||
<div className="space-y-4">
|
||||
{role === "user" ? (
|
||||
<InputComponent
|
||||
label="Name"
|
||||
type="text"
|
||||
id="name"
|
||||
isLabel
|
||||
placeholder="Vollständiger Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<InputComponent
|
||||
label="Vorname"
|
||||
type="text"
|
||||
id="firstName"
|
||||
isLabel
|
||||
placeholder="Vorname"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
<InputComponent
|
||||
label="Nachname"
|
||||
type="text"
|
||||
id="lastName"
|
||||
isLabel
|
||||
placeholder="Nachname"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(role === "user" || role === "manager") && (
|
||||
<InputComponent
|
||||
label="Telefonnummer"
|
||||
type="tel"
|
||||
id="tel"
|
||||
isLabel
|
||||
placeholder="+49 000 000000"
|
||||
value={tel}
|
||||
onChange={(e) => setTel(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{role === "user" && (
|
||||
<DropdownComponent label="Geschlecht" selected={gender} onSelect={setGender} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-1">
|
||||
<Button onClick={handleProfileSave} disabled={pending} size="sm" className="w-fit">
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ── E-Mail ──────────────────────────────────────────────────── */}
|
||||
<Card title="E-Mail-Adresse" subtitle="Änderungen erfordern eine Bestätigung per E-Mail">
|
||||
<InputComponent
|
||||
label="E-Mail"
|
||||
type="email"
|
||||
id="email"
|
||||
isLabel
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
onClick={handleEmailSave}
|
||||
disabled={pending || email === initialEmail}
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ── Adresse (nur reguläre Nutzer) ────────────────────────────
|
||||
Spans both columns on large screens so the 3-field layout
|
||||
has room to breathe. */}
|
||||
{role === "user" && (
|
||||
<div className="lg:col-span-2">
|
||||
<Card title="Adresse" subtitle="Straße, PLZ und Wohnort">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="sm:col-span-3">
|
||||
<AddressAutocompleteComponent
|
||||
streetValue={street}
|
||||
plzValue={plz}
|
||||
placeValue={city}
|
||||
onStreetChange={setStreet}
|
||||
onPlzChange={setPlz}
|
||||
onPlaceChange={setCity}
|
||||
onSelect={(parsed: ParsedAddress) => {
|
||||
setStreet(parsed.street);
|
||||
setPlz(parsed.plz);
|
||||
setCity(parsed.place);
|
||||
}}
|
||||
idPrefix="settings-address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<Button onClick={handleAddressSave} disabled={pending} size="sm" className="w-fit">
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Passwort ────────────────────────────────────────────────── */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card title="Passwort ändern" subtitle="Mindestens 8 Zeichen">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<InputComponent
|
||||
label="Aktuelles Passwort"
|
||||
type="password"
|
||||
id="currentPassword"
|
||||
isLabel
|
||||
placeholder="••••••••"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
<InputComponent
|
||||
label="Neues Passwort"
|
||||
type="password"
|
||||
id="newPassword"
|
||||
isLabel
|
||||
placeholder="••••••••"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<InputComponent
|
||||
label="Passwort bestätigen"
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
isLabel
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
onClick={handlePasswordSave}
|
||||
disabled={pending || !currentPassword || !newPassword || !confirmPassword}
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
>
|
||||
Passwort ändern
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalSettingsForm;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { requireAuthWithData, isManager } from "@/src/lib/auth/getUser";
|
||||
import { requireAuthWithData, isManager, isUser, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import SettingsMenuItem from "../components/SettingsMenuItem";
|
||||
import {
|
||||
Bell,
|
||||
|
|
@ -74,11 +74,23 @@ const settingsMenuTitles: SettingsMenuItem[] = [
|
|||
const SettingsOverviewView = async () => {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
if (!isManager(userData)) {
|
||||
if (isUser(userData) || isEmployee(userData)) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-1">Einstellungen</h1>
|
||||
<p className="text-gray-500 text-sm">Keine weiteren Einstellungen verfügbar.</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl flex gap-2 font-bold">
|
||||
<Settings /> Einstellungen
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Verwalte deine persönlichen Daten</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<SettingsMenuItem
|
||||
name="Persönliche Einstellungen"
|
||||
description="Profilbild, Name, E-Mail, Passwort und mehr"
|
||||
icon={User}
|
||||
href="/settings/personal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,58 @@
|
|||
const SettingsPersonalPageView = () => {
|
||||
return <div></div>;
|
||||
import { requireAuthWithData, isManager, isEmployee, isUser } from "@/src/lib/auth/getUser";
|
||||
import PersonalSettingsForm, { PersonalSettingsProps } from "../components/PersonalSettingsForm";
|
||||
|
||||
const SettingsPersonalPageView = async () => {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
let props: PersonalSettingsProps;
|
||||
|
||||
if (isManager(userData)) {
|
||||
props = {
|
||||
role: "manager",
|
||||
id: userData.id,
|
||||
email: userData.email,
|
||||
pbSrc: userData.pbSrc,
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
tel: userData.tel,
|
||||
};
|
||||
} else if (isEmployee(userData)) {
|
||||
props = {
|
||||
role: "employee",
|
||||
id: userData.id,
|
||||
email: userData.email,
|
||||
pbSrc: userData.pbSrc,
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
};
|
||||
} else if (isUser(userData)) {
|
||||
props = {
|
||||
role: "user",
|
||||
id: userData.id,
|
||||
email: userData.email,
|
||||
pbSrc: userData.pbSrc,
|
||||
name: userData.name,
|
||||
tel: userData.tel,
|
||||
gender: userData.gender,
|
||||
street: userData.street,
|
||||
plz: userData.plz,
|
||||
city: userData.city,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Persönliche Einstellungen</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Verwalte deine persönlichen Daten und Zugangsdaten
|
||||
</p>
|
||||
</div>
|
||||
<PersonalSettingsForm {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPersonalPageView;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue