see comments

This commit is contained in:
Rajpreet0 2026-05-05 10:45:05 +02:00
parent 8718700fd8
commit 87dd836ddc
10 changed files with 70 additions and 25 deletions

28
package-lock.json generated
View file

@ -28,6 +28,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.4.2",
"dotenv": "^17.2.3",
"lucide-react": "^0.552.0",
"next": "16.1.6",
@ -51,6 +52,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.0.5",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19",
@ -7945,6 +7947,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -8046,6 +8058,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -9927,6 +9946,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",

View file

@ -35,6 +35,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.4.2",
"dotenv": "^17.2.3",
"lucide-react": "^0.552.0",
"next": "16.1.6",
@ -58,6 +59,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.0.5",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19",

View file

@ -105,7 +105,6 @@ model Business {
updatedAt DateTime @updatedAt
@@index([managerId])
@@index([slug])
}
model Course {

View file

@ -1,12 +1,13 @@
import { PrismaClient } from "@prisma/client";
export async function generateUniqueSlug(name: string, prisma: PrismaClient): Promise<string> {
const base = name
const normalized = name
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.slice(0, 60);
let slug = base || "business";
const base = normalized || "business";
let slug = base;
let counter = 2;
while (await prisma.business.findUnique({ where: { slug } })) {
slug = `${base}-${counter++}`;

View file

@ -554,17 +554,29 @@ export async function toggleBusinessPublic(businessId: string, isPublic: boolean
return { success: false, error: "Business nicht gefunden" };
}
// Generate slug on first publish if missing
// Generate slug on first publish if missing, retry on P2002 race condition
let slug = business.slug;
if (!slug) {
slug = await generateUniqueSlug(business.name, prisma);
let updated = false;
while (!updated) {
if (!slug) {
slug = await generateUniqueSlug(business.name, prisma);
}
try {
await prisma.business.update({
where: { id: businessId },
data: { isPublic, slug },
});
updated = true;
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
// Another request claimed this slug concurrently — generate a new one and retry
slug = null;
} else {
throw err;
}
}
}
await prisma.business.update({
where: { id: businessId },
data: { isPublic, slug },
});
// Auto-assign courses that belong to this manager but have no businessId yet.
// Only do this when there is exactly one business — if there are multiple,
// the manager must assign courses manually via the course edit form.

View file

@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Button } from "@/src/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/src/components/ui/dialog";
import DOMPurify from "dompurify";
import { bookCourse, checkUserBookingStatus } from "../../actions/booking-actions";
import { toast } from "sonner";
@ -135,7 +136,7 @@ const BookingDialog = ({
{course.description && (
<div
className="prose prose-sm max-w-none text-gray-600 pt-1"
dangerouslySetInnerHTML={{ __html: course.description }}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(course.description) }}
/>
)}
</div>

View file

@ -103,6 +103,8 @@ const CourseCreateView = ({
const opts = result.businesses.map((b) => ({ value: b.id, label: b.name }));
setBusinesses(opts);
if (opts.length === 1) setSelectedBusinessId(opts[0].value);
} else {
toast.error("Fehler beim Laden der Standorte. Bitte laden Sie die Seite neu.");
}
};
loadBusinesses();

View file

@ -518,6 +518,10 @@ export async function updateEmployee(
revalidatePath("/employee");
revalidatePath(`/employee/edit/${employeeId}`);
invalidateUserDataCache(validatedData.email);
// If email changed, also invalidate the old email's cache entry
if (existingEmployee.email !== validatedData.email) {
invalidateUserDataCache(existingEmployee.email);
}
return {
success: true,

View file

@ -42,12 +42,16 @@ const BusinessPublicToggle = ({
setIsLoading(false);
};
const handleCopy = () => {
const handleCopy = async () => {
if (!publicUrl) return;
navigator.clipboard.writeText(publicUrl);
setCopied(true);
toast.success("Link kopiert");
setTimeout(() => setCopied(false), 2000);
try {
await navigator.clipboard.writeText(publicUrl);
setCopied(true);
toast.success("Link kopiert");
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Link konnte nicht kopiert werden");
}
};
return (

View file

@ -20,14 +20,6 @@ const SettingsView = async () => {
orderBy: { name: "asc" },
});
// If manager has exactly one business, auto-assign any unassigned courses to it
if (businesses.length === 1) {
await prisma.course.updateMany({
where: { createdBy: userData.id, businessId: null },
data: { businessId: businesses[0].id },
});
}
return (
<div className="space-y-6">
<div>