diff --git a/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/page.tsx b/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/page.tsx new file mode 100644 index 0000000..384d23a --- /dev/null +++ b/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/page.tsx @@ -0,0 +1,5 @@ +import { TenantLessonEditPage } from "./tenant-lesson-edit-page"; + +export default function LessonEditRoutePage() { + return ; +} diff --git a/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/tenant-lesson-edit-page.tsx b/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/tenant-lesson-edit-page.tsx new file mode 100644 index 0000000..1de8f0d --- /dev/null +++ b/apps/web/app/t/[tenantSlug]/(workspace)/lessons/[lessonId]/edit/tenant-lesson-edit-page.tsx @@ -0,0 +1,291 @@ +"use client"; + +import type { components } from "@studiqo/api-client/generated"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { isStudiqoApiError } from "@studiqo/api-client/errors"; +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; + +import { useLessonDetailQuery, useUpdateLessonMutation } from "@/lib/api/lessons-query"; +import { useOrganizationMembersQuery } from "@/lib/api/organization-members-query"; +import { useSubjectsListQuery } from "@/lib/api/subjects-query"; +import { + parseDatetimeLocalToIso, + parseIsoDateTime, + toDatetimeLocalValue, +} from "@/lib/datetime"; +import { formatOrgMemberOptionLabel } from "@/lib/format-org-member"; +import { useTenantOrganizationId } from "@/lib/hooks/use-tenant-organization"; +import { useSession } from "@/lib/auth/session"; +import { isOrgAdminOrSuperadmin } from "@/lib/tenant-role"; +import { + updateLessonFormSchema, + type UpdateLessonForm, +} from "@/lib/validation/lesson-forms"; + +type Lesson = components["schemas"]["Lesson"]; +type UpdateLessonBody = components["schemas"]["UpdateLessonRequest"]; + +function buildUpdateBody(lesson: Lesson, values: UpdateLessonForm): UpdateLessonBody { + const body: UpdateLessonBody = {}; + if ( + values.tutorId && + values.tutorId !== "" && + values.tutorId !== lesson.tutorId + ) { + body.tutorId = values.tutorId; + } + if ( + values.subjectId && + values.subjectId !== "" && + values.subjectId !== lesson.subjectId + ) { + body.subjectId = values.subjectId; + } + if (values.startsAtLocal?.trim()) { + const iso = parseDatetimeLocalToIso(values.startsAtLocal.trim()); + if (iso !== lesson.startsAt) { + body.startsAt = iso; + } + } + if (values.endsAtLocal?.trim()) { + const iso = parseDatetimeLocalToIso(values.endsAtLocal.trim()); + if (iso !== lesson.endsAt) { + body.endsAt = iso; + } + } + if (values.notes !== undefined) { + const next = values.notes === "" ? null : values.notes; + const prev = lesson.notes; + if (next !== prev) { + body.notes = next; + } + } + return body; +} + +export function TenantLessonEditPage() { + const params = useParams<{ tenantSlug: string; lessonId: string }>(); + const { tenantSlug, lessonId } = params; + const router = useRouter(); + const { user } = useSession(); + const { organizationId, orgsLoading } = useTenantOrganizationId(tenantSlug); + const canManage = isOrgAdminOrSuperadmin( + user?.role, + user?.isSuperadmin ?? false, + ); + const lessonQ = useLessonDetailQuery(organizationId, lessonId); + const subjectsQ = useSubjectsListQuery(organizationId); + const membersQ = useOrganizationMembersQuery(organizationId, canManage); + const updateLesson = useUpdateLessonMutation(organizationId ?? ""); + const [formError, setFormError] = useState(null); + + const form = useForm({ + resolver: zodResolver(updateLessonFormSchema), + defaultValues: { + tutorId: "", + subjectId: "", + startsAtLocal: "", + endsAtLocal: "", + notes: "", + }, + }); + + const lesson = lessonQ.data; + useEffect(() => { + if (lesson && lesson.status === "scheduled") { + form.reset({ + tutorId: lesson.tutorId, + subjectId: lesson.subjectId, + startsAtLocal: toDatetimeLocalValue(parseIsoDateTime(lesson.startsAt)), + endsAtLocal: toDatetimeLocalValue(parseIsoDateTime(lesson.endsAt)), + notes: lesson.notes ?? "", + }); + } + }, [lesson, form]); + + const base = `/t/${tenantSlug}/lessons`; + + if (!canManage) { + return ( +
+

Edit lesson

+

+ Only organization admins can edit lessons. +

+

+ Back to lesson +

+
+ ); + } + + if (orgsLoading || !organizationId) { + return ( +
+

+ ← Lesson +

+

Loading…

+
+ ); + } + + if (lessonQ.isLoading) { + return ( +
+

+ ← Lesson +

+

Loading lesson…

+
+ ); + } + + if (lessonQ.error || !lesson) { + return ( +
+

+ ← Lessons +

+

+ {lessonQ.error instanceof Error + ? lessonQ.error.message + : "Could not load lesson"} +

+
+ ); + } + + if (lesson.status !== "scheduled") { + return ( +
+

+ ← Lesson +

+

Edit lesson

+

+ Only scheduled lessons can be edited. This lesson is{" "} + {lesson.status}. +

+
+ ); + } + + const editingLesson = lesson; + const tutors = membersQ.data?.filter((m) => m.role === "tutor") ?? []; + + async function onSubmit(values: UpdateLessonForm) { + setFormError(null); + const body = buildUpdateBody(editingLesson, values); + if (Object.keys(body).length === 0) { + setFormError("Change at least one field."); + return; + } + try { + await updateLesson.mutateAsync({ lessonId: editingLesson.id, body }); + router.push(`${base}/${editingLesson.id}`); + } catch (e) { + if (isStudiqoApiError(e)) setFormError(e.message); + else setFormError("Could not update lesson"); + } + } + + const loadingRefs = subjectsQ.isLoading || membersQ.isLoading; + + return ( +
+

+ ← Lesson +

+

Edit lesson

+ + {loadingRefs ?

Loading…

: null} + +
+ + + + + + + + +