added trainer in course creation

This commit is contained in:
Rajpreet0 2026-04-15 20:28:52 +02:00
parent 25d4fb2cc7
commit 3c05a0fdae
4 changed files with 127 additions and 29 deletions

View file

@ -25,6 +25,7 @@ interface CourseListItemProps {
price: number; // Preis in Euro
level: string; // Level
trainers: string[];
trainersMap?: Record<string, { label: string; pbSrc?: string }>;
onEdit?: () => void; // Callback-Funktion beim Klick auf Bearbeiten-Button
onDelete?: () => void; // Callback-Funktion beim Klick auf Löschen-Button
status?: string; // Status des Kurses ("draft" oder "published")
@ -47,6 +48,7 @@ const CourseListItem: React.FC<CourseListItemProps> = ({
price,
level,
trainers,
trainersMap = {},
onEdit,
onDelete,
status,
@ -90,20 +92,24 @@ const CourseListItem: React.FC<CourseListItemProps> = ({
}`}
>
<div className="flex items-center w-full">
{/*TODO: Avatar */}
{trainers.map((trainer, index) => (
<AbstractTooltip key={`${trainer}-${index}`} tooltipText={trainer}>
<div
className={`relative ${index !== 0 ? "-ml-3" : ""}`}
style={{ zIndex: trainers.length - index }}
>
<Avatar className="rounded-full border-2 border-white">
<AvatarImage src="https://github.com/shadcn.png" alt={trainer} />
<AvatarFallback>{getInitials(trainer)}</AvatarFallback>
</Avatar>
</div>
</AbstractTooltip>
))}
{trainers.map((trainerId, index) => {
const trainerInfo = trainersMap[trainerId];
const displayName = trainerInfo?.label ?? trainerId;
const avatarSrc = trainerInfo?.pbSrc;
return (
<AbstractTooltip key={`${trainerId}-${index}`} tooltipText={displayName}>
<div
className={`relative ${index !== 0 ? "-ml-3" : ""}`}
style={{ zIndex: trainers.length - index }}
>
<Avatar className="rounded-full border-2 border-white">
{avatarSrc && <AvatarImage src={avatarSrc} alt={displayName} />}
<AvatarFallback>{getInitials(displayName)}</AvatarFallback>
</Avatar>
</div>
</AbstractTooltip>
);
})}
{/* Time */}
<div className="w-full flex justify-end">
<p className="text-gray-500 text-lg">

View file

@ -10,19 +10,13 @@ import { Checkbox } from "@/src/components/ui/checkbox";
import { Label } from "@/src/components/ui/label";
import { createCourse, updateCourse } from "../../actions/course-actions";
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions";
import { toast } from "sonner";
import { Course } from "../../types/course.types";
import { Trash2 } from "lucide-react";
import DeleteDialog from "@/src/components/layout/DeleteDialog";
import { useDeleteCourse } from "../../hooks/useDeleteCourse";
const TRAINERS = [
{ value: "trainer1", label: "Max Mustermann" },
{ value: "trainer2", label: "Anna Schmidt" },
{ value: "trainer3", label: "Peter Weber" },
{ value: "trainer4", label: "Lisa Müller" },
];
const LEVELS = [
{ value: "any", label: "Jedes Niveau" },
{ value: "beginner", label: "Anfänger" },
@ -84,6 +78,8 @@ const CourseCreateView = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const [rooms, setRooms] = useState<Array<{ value: string; label: string }>>([]);
const [isLoadingRooms, setIsLoadingRooms] = useState(true);
const [trainers, setTrainers] = useState<Array<{ value: string; label: string }>>([]);
const [isLoadingTrainers, setIsLoadingTrainers] = useState(true);
const {
deleteDialogOpen,
@ -116,6 +112,19 @@ const CourseCreateView = ({
loadRooms();
}, []);
// Load trainers from database
useEffect(() => {
const loadTrainers = async () => {
setIsLoadingTrainers(true);
const result = await getMyTrainers();
if (result.success) {
setTrainers(result.trainers);
}
setIsLoadingTrainers(false);
};
loadTrainers();
}, []);
// Load initial data when in edit mode
useEffect(() => {
if (mode === "edit" && initialData) {
@ -168,7 +177,7 @@ const CourseCreateView = ({
newErrors.timeTo = "Endzeit muss nach der Anfangszeit liegen";
}
if (selectedTrainers.length === 0)
if (trainers.length > 0 && selectedTrainers.length === 0)
newErrors.trainers = "Mindestens ein Trainer muss ausgewählt werden";
if (!selectedRoom) newErrors.room = "Raum ist erforderlich";
@ -405,13 +414,17 @@ const CourseCreateView = ({
{errors.price && <p className="text-red-500 text-sm mt-1">{errors.price}</p>}
</div>
<MultiSelectDropdown
label="Trainer"
selected={selectedTrainers}
onSelect={setSelectedTrainers}
options={TRAINERS}
error={errors.trainers}
/>
{!isLoadingTrainers && trainers.length > 0 && (
<div>
<MultiSelectDropdown
label="Trainer"
selected={selectedTrainers}
onSelect={setSelectedTrainers}
options={trainers}
error={errors.trainers}
/>
</div>
)}
<GenericDropdown
label="Raum"

View file

@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { getMyCourses } from "../../actions/course-actions";
import { useDeleteCourse } from "../../hooks/useDeleteCourse";
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions";
import { Button } from "@/src/components/ui/button";
import { toast } from "sonner";
import { useRouter, useSearchParams } from "next/navigation";
@ -60,6 +61,9 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
const [priceMax, setPriceMax] = useState<number | null>(null);
const [deleteMode, setDeleteMode] = useState(false);
const [roomsMap, setRoomsMap] = useState<Record<string, string>>({});
const [trainersMap, setTrainersMap] = useState<Record<string, { label: string; pbSrc?: string }>>(
{}
);
const {
deleteDialogOpen,
@ -100,6 +104,21 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
loadRooms();
}, []);
// Load trainers once on mount
useEffect(() => {
const loadTrainers = async () => {
const result = await getMyTrainers();
if (result.success) {
const map: Record<string, { label: string; pbSrc?: string }> = {};
result.trainers.forEach((t) => {
map[t.value] = { label: t.label, pbSrc: t.pbSrc ?? undefined };
});
setTrainersMap(map);
}
};
loadTrainers();
}, []);
/**
* Load courses with optional date range
* @param options - Optional date range parameters
@ -583,6 +602,7 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
price={course.price}
level={course.level}
trainers={course.trainers}
trainersMap={trainersMap}
status={course.status}
isPast={isPastCourse(course.date)}
showDeleteIcon={deleteMode && canDelete}

View file

@ -195,6 +195,65 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
}
}
/**
* Ruft alle Trainer (Mitarbeiter mit Rolle "trainer") ab, die vom eingeloggten Manager erstellt wurden
*
* @returns Ein Objekt mit success-Flag und einem Array von Trainern im Format {value, label}
*/
export async function getMyTrainers() {
try {
const userData = await getUserData();
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.view))
) {
return {
success: false,
error: "Unauthorized",
trainers: [],
};
}
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
const employees = await prisma.employee.findMany({
where: {
createdBy: managerId,
roles: { has: "trainer" },
isOnboarded: true,
},
select: {
id: true,
firstName: true,
lastName: true,
email: true,
pbSrc: true,
},
orderBy: { firstName: "asc" },
});
const trainers = employees.map((e) => ({
value: e.id,
label: e.firstName && e.lastName ? `${e.firstName} ${e.lastName}` : e.email,
pbSrc: e.pbSrc ?? undefined,
}));
return { success: true, trainers };
} catch (error) {
console.error("Error fetching trainers:", error);
return {
success: false,
error: "Fehler beim Laden der Trainer",
trainers: [],
};
}
}
/**
* Ruft alle Mitarbeiter ab, die vom eingeloggten Manager erstellt wurden
*