diff --git a/backend/app/controllers/schedules_controller.ts b/backend/app/controllers/schedules_controller.ts index 748e2bb..e600a0c 100644 --- a/backend/app/controllers/schedules_controller.ts +++ b/backend/app/controllers/schedules_controller.ts @@ -148,6 +148,8 @@ export default class SchedulesController { .andWhere('userId', userId) .firstOrFail() + console.log(payload) + if (payload.name) { currSchedule.name = payload.name } @@ -179,12 +181,12 @@ export default class SchedulesController { } if (payload.updatedAt) { - currSchedule.updatedAt = DateTime.fromJSDate(payload.updatedAt) + currSchedule.updatedAt = DateTime.fromISO(payload.updatedAt) } await currSchedule.save() - return { message: 'Schedule updated successfully.', currSchedule } + return { message: 'Schedule updated successfully.', currSchedule, payload } } /** diff --git a/backend/app/validators/schedule.ts b/backend/app/validators/schedule.ts index 7aaef34..25c1ac7 100644 --- a/backend/app/validators/schedule.ts +++ b/backend/app/validators/schedule.ts @@ -30,6 +30,6 @@ export const updateScheduleValidator = vine.compile( }) ) .optional(), - updatedAt: vine.date().optional(), + updatedAt: vine.string().optional(), }) ) diff --git a/frontend/src/actions/plans.ts b/frontend/src/actions/plans.ts index 0bfd718..dfb44c8 100644 --- a/frontend/src/actions/plans.ts +++ b/frontend/src/actions/plans.ts @@ -15,6 +15,39 @@ interface CreatePlanResponseType { }; } +interface PlanResponseType { + name: string; + userId: number; + id: number; + createdAt: string; + updatedAt: string; + courses: Array<{ + id: string; + name: string; + department: string; + lecturer: string; + type: string; + ects: number; + semester: number; + groups: Array<{ + id: number; + name: string; + day: string; + time: string; + room: string; + }>; + }>; + registrations: Array<{ + id: string; + name: string; + }>; +} + +interface DeletePlanResponseType { + success: boolean; + message: string; +} + export const createNewPlan = async ({ name }: { name: string }) => { const isLogged = await auth({}); if (!isLogged) { @@ -38,12 +71,14 @@ export const updatePlan = async ({ courses, registrations, groups, + updatedAt, }: { id: number; name: string; courses: Array<{ id: string }>; registrations: Array<{ id: string }>; groups: Array<{ id: number }>; + updatedAt: string; }) => { const isLogged = await auth({}); if (!isLogged) { @@ -53,7 +88,7 @@ export const updatePlan = async ({ const data = await fetchToAdonis({ url: `/user/schedules/${id}`, method: "PATCH", - body: JSON.stringify({ name, courses, registrations, groups }), + body: JSON.stringify({ name, courses, registrations, groups, updatedAt }), }); if (!data) { return false; @@ -62,20 +97,38 @@ export const updatePlan = async ({ }; export const deletePlan = async ({ id }: { id: number }) => { - console.log(id); const isLogged = await auth({}); if (!isLogged) { return false; } - const data = await fetchToAdonis({ + const data = await fetchToAdonis({ url: `/user/schedules/${id}`, method: "DELETE", }); - console.log(data); - if (!data || data.success !== true) { + if (!(data?.success ?? false)) { return false; } revalidatePath("/plans"); return data; }; + +export const getPlan = async ({ id }: { id: number }) => { + const isLogged = await auth({}); + if (!isLogged) { + return false; + } + + try { + const data = await fetchToAdonis({ + url: `/user/schedules/${id}`, + method: "GET", + }); + if (!data) { + return false; + } + return data; + } catch (e) { + return false; + } +}; diff --git a/frontend/src/app/plans/_components/PlansPage.tsx b/frontend/src/app/plans/_components/PlansPage.tsx index 18bd0aa..fc6b983 100644 --- a/frontend/src/app/plans/_components/PlansPage.tsx +++ b/frontend/src/app/plans/_components/PlansPage.tsx @@ -3,13 +3,10 @@ import { atom, useAtom } from "jotai"; import { PlusIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useEffect, useRef } from "react"; -import type { ExtendedCourse } from "@/atoms/planFamily"; import { planFamily } from "@/atoms/planFamily"; import { plansIds } from "@/atoms/plansIds"; import { PlanItem } from "@/components/PlanItem"; -import type { Registration } from "@/lib/types"; import type { PlanResponseDataType } from "../page"; @@ -26,7 +23,6 @@ export function PlansPage({ }) { const [plans, setPlans] = useAtom(plansAtom); const router = useRouter(); - const firstTime = useRef(true); const addNewPlan = () => { const uuid = crypto.randomUUID(); @@ -42,46 +38,6 @@ export function PlansPage({ setPlans([...plans, newPlan]); }; - const handleCreateOfflinePlansIfNotExists = () => { - if (firstTime.current) { - firstTime.current = false; - const tmpPlans: Array<{ - id: string; - name: string; - synced: boolean; - onlineId: string; - courses: ExtendedCourse[]; - registrations: Registration[]; - createdAt: Date; - updatedAt: Date; - }> = []; - onlinePlans.forEach((onlinePlan) => { - if (plans.some((plan) => plan.onlineId === onlinePlan.id.toString())) { - return; - } - - const uuid = crypto.randomUUID(); - const newPlan = { - id: uuid, - name: onlinePlan.name, - synced: true, - onlineId: onlinePlan.id.toString(), - courses: onlinePlan.courses, - registrations: onlinePlan.registrations, - createdAt: new Date(onlinePlan.createdAt), - updatedAt: new Date(onlinePlan.updatedAt), - }; - - tmpPlans.push(newPlan); - }); - setPlans([...plans, ...tmpPlans]); - } - }; - - // useEffect(() => { - // handleCreateOfflinePlansIfNotExists(); - // }, []); - return (
@@ -96,7 +52,7 @@ export function PlansPage({ key={plan.id} id={plan.id} name={plan.name} - synced={false} + synced={plan.synced} onlineId={plan.onlineId} /> ))} @@ -112,6 +68,8 @@ export function PlansPage({ synced={true} onlineId={plan.id.toString()} onlineOnly={true} + groupCount={plan.courses.flatMap((c) => c.groups).length} + registrationCount={plan.registrations.length} /> ); })} diff --git a/frontend/src/app/plans/edit/[id]/_components/CreateNewPlanPage.tsx b/frontend/src/app/plans/edit/[id]/_components/CreateNewPlanPage.tsx index 39ea18f..02a7aab 100644 --- a/frontend/src/app/plans/edit/[id]/_components/CreateNewPlanPage.tsx +++ b/frontend/src/app/plans/edit/[id]/_components/CreateNewPlanPage.tsx @@ -1,12 +1,13 @@ "use client"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { format } from "date-fns"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { MdArrowBack } from "react-icons/md"; import { toast } from "sonner"; -import { createNewPlan, updatePlan } from "@/actions/plans"; +import { createNewPlan, getPlan, updatePlan } from "@/actions/plans"; import type { ExtendedCourse, ExtendedGroup } from "@/atoms/planFamily"; import { ClassSchedule } from "@/components/ClassSchedule"; import { GroupsAccordionItem } from "@/components/GroupsAccordion"; @@ -24,7 +25,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; -import { env } from "@/env.mjs"; import { usePlan } from "@/lib/usePlan"; import { registrationReplacer } from "@/lib/utils"; import type { LessonType } from "@/services/usos/types"; @@ -82,6 +82,7 @@ export function CreateNewPlanPage({ const handleSyncPlan = async () => { setSyncing(true); + const updatedAtDate = new Date(); try { const res = await updatePlan({ id: Number(plan.onlineId), @@ -93,12 +94,17 @@ export function CreateNewPlanPage({ groups: plan.allGroups .filter((g) => g.isChecked) .map((g) => ({ id: g.groupOnlineId })), + updatedAt: updatedAtDate.toISOString(), }); if (res === false) { return toast.error("Nie udało się zaktualizować planu"); } toast.success("Zaktualizowano plan"); - plan.setSynced(true); + plan.setPlan((prev) => ({ + ...prev, + synced: true, + updatedAt: updatedAtDate, + })); return true; } finally { setSyncing(false); @@ -111,12 +117,6 @@ export function CreateNewPlanPage({ } }, [plan]); - // useEffect(() => { - // if (!plan.synced && plan.onlineId !== null && !firstTime.current) { - // void handleUpdatePlan(); - // } - // }, [plan.onlineId, plan.synced, firstTime]); - const inputRef = useRef(null); const [faculty, setFaculty] = useState(null); @@ -125,7 +125,7 @@ export function CreateNewPlanPage({ queryKey: ["registrations", faculty], queryFn: async () => { const response = await fetch( - `${env.NEXT_PUBLIC_API_URL}/departments/${faculty}/registrations`, + `${process.env.NEXT_PUBLIC_API_URL}/departments/${faculty}/registrations`, ); if (!response.ok) { @@ -140,7 +140,7 @@ export function CreateNewPlanPage({ mutationKey: ["courses"], mutationFn: async (registrationId: string) => { const response = await fetch( - `${env.NEXT_PUBLIC_API_URL}/departments/${faculty}/registrations/${encodeURIComponent(registrationId)}/courses`, + `${process.env.NEXT_PUBLIC_API_URL}/departments/${faculty}/registrations/${encodeURIComponent(registrationId)}/courses`, ); if (!response.ok) { @@ -151,6 +151,61 @@ export function CreateNewPlanPage({ }, }); + const handleUpdateLocalPlan = async () => { + firstTime.current = false; + const onlinePlan = await getPlan({ id: Number(plan.onlineId) }); + if (onlinePlan === false) { + return toast.error("Nie udało się pobrać planu"); + } + for (const registration of onlinePlan.registrations) { + coursesFn.mutate(registration.id, { + onSuccess: (courses) => { + const extendedCourses: ExtendedCourse[] = courses + .map((c) => { + const groups: ExtendedGroup[] = c.groups.map((g) => ({ + groupId: g.group + c.id + g.type, + groupNumber: g.group.toString(), + groupOnlineId: g.id, + courseId: c.id, + courseName: c.name, + isChecked: + onlinePlan.courses + .find((oc) => oc.id === c.id) + ?.groups.some((og) => og.id === g.id) ?? false, + courseType: g.type, + day: g.day, + lecturer: g.lecturer, + registrationId: c.registrationId, + week: g.week.replace("-", "") as "" | "TN" | "TP", + endTime: g.endTime.split(":").slice(0, 2).join(":"), + startTime: g.startTime.split(":").slice(0, 2).join(":"), + })); + return { + id: c.id, + name: c.name, + isChecked: onlinePlan.courses.some((oc) => oc.id === c.id), + registrationId: c.registrationId, + type: c.groups.at(0)?.type ?? ("" as LessonType), + groups, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + plan.addRegistration(registration, extendedCourses, true); + }, + onError: () => { + toast.error("Nie udało się pobrać kursów"); + }, + }); + } + return true; + }; + + useEffect(() => { + if (plan.onlineId !== null && plan.toCreate && firstTime.current) { + void handleUpdateLocalPlan(); + } + }, [plan]); + return (
diff --git a/frontend/src/app/plans/edit/[id]/page.tsx b/frontend/src/app/plans/edit/[id]/page.tsx index b2a3ad5..234bbda 100644 --- a/frontend/src/app/plans/edit/[id]/page.tsx +++ b/frontend/src/app/plans/edit/[id]/page.tsx @@ -2,8 +2,6 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import React from "react"; -import { env } from "@/env.mjs"; - import { CreateNewPlanPage } from "./_components/CreateNewPlanPage"; interface PageProps { @@ -21,7 +19,7 @@ export default async function CreateNewPlan({ params }: PageProps) { } const facultiesRes = (await fetch( - `${env.NEXT_PUBLIC_API_URL}/departments`, + `${process.env.NEXT_PUBLIC_API_URL}/departments`, ).then((r) => r.json())) as Array<{ id: string; name: string }> | null; if (!facultiesRes) { return notFound(); diff --git a/frontend/src/atoms/planFamily.ts b/frontend/src/atoms/planFamily.ts index 9b92d8c..bc0fb4c 100644 --- a/frontend/src/atoms/planFamily.ts +++ b/frontend/src/atoms/planFamily.ts @@ -21,7 +21,9 @@ export const planFamily = atomFamily( courses: [] as ExtendedCourse[], registrations: [] as Registration[], createdAt: new Date(), + updatedAt: new Date(), onlineId: null as string | null, + toCreate: true as boolean, synced: false, }, undefined, diff --git a/frontend/src/components/PlanItem.tsx b/frontend/src/components/PlanItem.tsx index 1c4b821..01fe05f 100644 --- a/frontend/src/components/PlanItem.tsx +++ b/frontend/src/components/PlanItem.tsx @@ -3,13 +3,10 @@ import { format } from "date-fns"; import { useAtom } from "jotai"; import { - AlertTriangleIcon, - CloudIcon, CopyIcon, EllipsisVerticalIcon, Loader2Icon, Pencil, - RefreshCwOffIcon, TrashIcon, } from "lucide-react"; import Link from "next/link"; @@ -47,6 +44,7 @@ import { CardHeader, CardTitle, } from "./ui/card"; +import { StatusIcon } from "./ui/status-icon"; export const PlanItem = ({ id, @@ -54,16 +52,21 @@ export const PlanItem = ({ synced, onlineId, onlineOnly = false, + groupCount = 0, + registrationCount = 0, }: { id: string; name: string; synced: boolean; onlineId: string | null; onlineOnly?: boolean; + groupCount?: number; + registrationCount?: number; }) => { const uuid = React.useMemo(() => crypto.randomUUID(), []); + const uuidToCopy = React.useMemo(() => crypto.randomUUID(), []); const [plans, setPlans] = useAtom(plansIds); - const plan = usePlan({ planId: id }); + const plan = usePlan({ planId: onlineOnly ? uuid : id }); const planToCopy = usePlan({ planId: uuid }); const router = useRouter(); const [dialogOpened, setDialogOpened] = React.useState(false); @@ -74,7 +77,7 @@ export const PlanItem = ({ setDropdownOpened(false); const newPlan = { - id: uuid, + id: uuidToCopy, }; void window.umami?.track("Create plan", { @@ -92,6 +95,24 @@ export const PlanItem = ({ }, 200); }; + const createFromOnlinePlan = () => { + const newPlan = { + id: uuid, + }; + + setPlans([...plans, newPlan]); + plan.setPlan({ + ...plan, + id: uuid, + onlineId, + name, + }); + + setTimeout(() => { + router.push(`/plans/edit/${newPlan.id}`); + }, 200); + }; + const handleDeletePlan = async () => { setLoading(true); plan.remove(); @@ -104,13 +125,13 @@ export const PlanItem = ({ toast.success("Plan został usunięty."); }; - const groupCount = plan.courses + const groupCountLocal = plan.courses .flatMap((c) => c.groups) .filter((group) => group.isChecked).length; return ( - + {name} {format( @@ -121,13 +142,18 @@ export const PlanItem = ({

- {plan.registrations.length}{" "} - {pluralize(plan.registrations.length, "kurs", "kursy", "kursów")} + {registrationCount || plan.registrations.length}{" "} + {pluralize( + registrationCount || plan.registrations.length, + "kurs", + "kursy", + "kursów", + )}

- {groupCount}{" "} + {groupCount || groupCountLocal}{" "} {pluralize( - groupCount, + groupCount || groupCountLocal, "wybrana grupa", "wybrane grupy", "wybranych grup", @@ -159,23 +185,22 @@ export const PlanItem = ({ - - - -

- {synced ? ( - - ) : !(onlineId ?? "") ? ( - + ) : ( - + )} -
+ + + { + return ( +
+ {synced ? ( + + ) : !(onlineId ?? "") ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend/src/lib/usePlan.ts b/frontend/src/lib/usePlan.ts index 2267f6c..62831f8 100644 --- a/frontend/src/lib/usePlan.ts +++ b/frontend/src/lib/usePlan.ts @@ -43,14 +43,19 @@ export const usePlan = ({ planId }: { planId: string }) => { addRegistration: ( registration: Registration, courses: ExtendedCourse[], + firstTime = false, ) => { setPlan({ ...plan, registrations: [...plan.registrations, registration].filter( (r, i, a) => a.findIndex((t) => t.id === r.id) === i, ), - courses: [...plan.courses, ...courses], - synced: false, + courses: [...plan.courses, ...courses].filter( + (c, i, a) => a.findIndex((t) => t.id === c.id) === i, + ), + + synced: firstTime, + toCreate: false, }); }, removeRegistration: (registrationId: string) => {