mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #81 from Since-3/feature/added-trainer-course-creation
added trainer in course creation
This commit is contained in:
commit
5c4d76a281
4 changed files with 127 additions and 29 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue