Add lessons module, rate cycles, EC2 deploy scripts, and help content
- Lessons module: lesson types, instructors, schedule slots, enrollments, sessions (list + week grid view), lesson plans, grading scales, templates - Rate cycles: replace monthly_rate with billing_interval + billing_unit on enrollments; add weekly/monthly/quarterly rate presets to lesson types and schedule slots with auto-fill on enrollment form - Member detail page: tabbed layout for details, identity documents, enrollments - Sessions week view: custom 7-column grid replacing react-big-calendar - Music store seed: instructors, lesson types, slots, enrollments, sessions, grading scale, lesson plan template - Scrollbar styling: themed to match sidebar/app palette - deploy/: EC2 setup and redeploy scripts, nginx config, systemd service - Help: add Lessons category (overview, types, instructors, slots, enrollments, sessions, plans/grading); collapsible sidebar with independent scroll; remove POS/accounting references from docs
This commit is contained in:
341
packages/admin/src/api/lessons.ts
Normal file
341
packages/admin/src/api/lessons.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type {
|
||||
Instructor,
|
||||
InstructorBlockedDate,
|
||||
LessonType,
|
||||
ScheduleSlot,
|
||||
Enrollment,
|
||||
LessonSession,
|
||||
GradingScale,
|
||||
LessonPlan,
|
||||
LessonPlanItem,
|
||||
LessonPlanItemGradeHistory,
|
||||
LessonPlanTemplate,
|
||||
StoreClosure,
|
||||
SessionPlanItem,
|
||||
} from '@/types/lesson'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Instructors ---
|
||||
|
||||
export const instructorKeys = {
|
||||
all: ['instructors'] as const,
|
||||
list: (params: PaginationInput) => [...instructorKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...instructorKeys.all, 'detail', id] as const,
|
||||
blockedDates: (id: string) => [...instructorKeys.all, id, 'blocked-dates'] as const,
|
||||
}
|
||||
|
||||
export function instructorListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: instructorKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Instructor>>('/v1/instructors', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function instructorDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: instructorKeys.detail(id),
|
||||
queryFn: () => api.get<Instructor>(`/v1/instructors/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function instructorBlockedDatesOptions(instructorId: string) {
|
||||
return queryOptions({
|
||||
queryKey: instructorKeys.blockedDates(instructorId),
|
||||
queryFn: () => api.get<InstructorBlockedDate[]>(`/v1/instructors/${instructorId}/blocked-dates`),
|
||||
enabled: !!instructorId,
|
||||
})
|
||||
}
|
||||
|
||||
export const instructorMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<Instructor>('/v1/instructors', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<Instructor>(`/v1/instructors/${id}`, data),
|
||||
delete: (id: string) =>
|
||||
api.del<Instructor>(`/v1/instructors/${id}`),
|
||||
addBlockedDate: (instructorId: string, data: Record<string, unknown>) =>
|
||||
api.post<InstructorBlockedDate>(`/v1/instructors/${instructorId}/blocked-dates`, data),
|
||||
deleteBlockedDate: (instructorId: string, id: string) =>
|
||||
api.del<InstructorBlockedDate>(`/v1/instructors/${instructorId}/blocked-dates/${id}`),
|
||||
}
|
||||
|
||||
// --- Lesson Types ---
|
||||
|
||||
export const lessonTypeKeys = {
|
||||
all: ['lesson-types'] as const,
|
||||
list: (params: PaginationInput) => [...lessonTypeKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...lessonTypeKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function lessonTypeListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: lessonTypeKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<LessonType>>('/v1/lesson-types', params),
|
||||
})
|
||||
}
|
||||
|
||||
export const lessonTypeMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<LessonType>('/v1/lesson-types', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<LessonType>(`/v1/lesson-types/${id}`, data),
|
||||
delete: (id: string) =>
|
||||
api.del<LessonType>(`/v1/lesson-types/${id}`),
|
||||
}
|
||||
|
||||
// --- Schedule Slots ---
|
||||
|
||||
export const scheduleSlotKeys = {
|
||||
all: ['schedule-slots'] as const,
|
||||
list: (params: PaginationInput) => [...scheduleSlotKeys.all, 'list', params] as const,
|
||||
byInstructor: (instructorId: string, params: PaginationInput) =>
|
||||
[...scheduleSlotKeys.all, 'instructor', instructorId, params] as const,
|
||||
detail: (id: string) => [...scheduleSlotKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function scheduleSlotListOptions(params: PaginationInput, filters?: { instructorId?: string; dayOfWeek?: number }) {
|
||||
const query = { ...params, ...filters }
|
||||
return queryOptions({
|
||||
queryKey: filters?.instructorId
|
||||
? scheduleSlotKeys.byInstructor(filters.instructorId, params)
|
||||
: scheduleSlotKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<ScheduleSlot>>('/v1/schedule-slots', query),
|
||||
})
|
||||
}
|
||||
|
||||
export const scheduleSlotMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<ScheduleSlot>('/v1/schedule-slots', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<ScheduleSlot>(`/v1/schedule-slots/${id}`, data),
|
||||
delete: (id: string) =>
|
||||
api.del<ScheduleSlot>(`/v1/schedule-slots/${id}`),
|
||||
}
|
||||
|
||||
// --- Enrollments ---
|
||||
|
||||
export const enrollmentKeys = {
|
||||
all: ['enrollments'] as const,
|
||||
list: (params: Record<string, unknown>) => [...enrollmentKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...enrollmentKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function enrollmentListOptions(params: Record<string, unknown>) {
|
||||
return queryOptions({
|
||||
queryKey: enrollmentKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Enrollment>>('/v1/enrollments', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function enrollmentDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: enrollmentKeys.detail(id),
|
||||
queryFn: () => api.get<Enrollment>(`/v1/enrollments/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const enrollmentMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<Enrollment>('/v1/enrollments', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<Enrollment>(`/v1/enrollments/${id}`, data),
|
||||
updateStatus: (id: string, status: string) =>
|
||||
api.post<Enrollment>(`/v1/enrollments/${id}/status`, { status }),
|
||||
generateSessions: (id: string, weeks?: number) =>
|
||||
api.post<{ generated: number; sessions: LessonSession[] }>(
|
||||
`/v1/enrollments/${id}/generate-sessions${weeks ? `?weeks=${weeks}` : ''}`,
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
// --- Lesson Sessions ---
|
||||
|
||||
export const sessionKeys = {
|
||||
all: ['lesson-sessions'] as const,
|
||||
list: (params: Record<string, unknown>) => [...sessionKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...sessionKeys.all, 'detail', id] as const,
|
||||
planItems: (id: string) => [...sessionKeys.all, id, 'plan-items'] as const,
|
||||
}
|
||||
|
||||
export function sessionListOptions(params: Record<string, unknown>) {
|
||||
return queryOptions({
|
||||
queryKey: sessionKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<LessonSession>>('/v1/lesson-sessions', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function sessionDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: sessionKeys.detail(id),
|
||||
queryFn: () => api.get<LessonSession>(`/v1/lesson-sessions/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function sessionPlanItemsOptions(sessionId: string) {
|
||||
return queryOptions({
|
||||
queryKey: sessionKeys.planItems(sessionId),
|
||||
queryFn: () => api.get<SessionPlanItem[]>(`/v1/lesson-sessions/${sessionId}/plan-items`),
|
||||
enabled: !!sessionId,
|
||||
})
|
||||
}
|
||||
|
||||
export const sessionMutations = {
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<LessonSession>(`/v1/lesson-sessions/${id}`, data),
|
||||
updateStatus: (id: string, status: string) =>
|
||||
api.post<LessonSession>(`/v1/lesson-sessions/${id}/status`, { status }),
|
||||
updateNotes: (id: string, data: Record<string, unknown>) =>
|
||||
api.post<LessonSession>(`/v1/lesson-sessions/${id}/notes`, data),
|
||||
linkPlanItems: (id: string, lessonPlanItemIds: string[]) =>
|
||||
api.post<{ linked: number; items: SessionPlanItem[] }>(`/v1/lesson-sessions/${id}/plan-items`, { lessonPlanItemIds }),
|
||||
}
|
||||
|
||||
// --- Grading Scales ---
|
||||
|
||||
export const gradingScaleKeys = {
|
||||
all: ['grading-scales'] as const,
|
||||
list: (params: PaginationInput) => [...gradingScaleKeys.all, 'list', params] as const,
|
||||
allScales: [...['grading-scales'], 'all'] as const,
|
||||
detail: (id: string) => [...gradingScaleKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function gradingScaleListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: gradingScaleKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<GradingScale>>('/v1/grading-scales', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function gradingScaleAllOptions() {
|
||||
return queryOptions({
|
||||
queryKey: gradingScaleKeys.allScales,
|
||||
queryFn: () => api.get<GradingScale[]>('/v1/grading-scales/all'),
|
||||
})
|
||||
}
|
||||
|
||||
export function gradingScaleDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: gradingScaleKeys.detail(id),
|
||||
queryFn: () => api.get<GradingScale>(`/v1/grading-scales/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const gradingScaleMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<GradingScale>('/v1/grading-scales', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<GradingScale>(`/v1/grading-scales/${id}`, data),
|
||||
delete: (id: string) =>
|
||||
api.del<GradingScale>(`/v1/grading-scales/${id}`),
|
||||
}
|
||||
|
||||
// --- Lesson Plans ---
|
||||
|
||||
export const lessonPlanKeys = {
|
||||
all: ['lesson-plans'] as const,
|
||||
list: (params: Record<string, unknown>) => [...lessonPlanKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...lessonPlanKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function lessonPlanListOptions(params: Record<string, unknown>) {
|
||||
return queryOptions({
|
||||
queryKey: lessonPlanKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<LessonPlan>>('/v1/lesson-plans', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function lessonPlanDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: lessonPlanKeys.detail(id),
|
||||
queryFn: () => api.get<LessonPlan>(`/v1/lesson-plans/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const lessonPlanMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<LessonPlan>('/v1/lesson-plans', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<LessonPlan>(`/v1/lesson-plans/${id}`, data),
|
||||
}
|
||||
|
||||
// --- Lesson Plan Items ---
|
||||
|
||||
export const lessonPlanItemKeys = {
|
||||
gradeHistory: (itemId: string) => ['lesson-plan-items', itemId, 'grade-history'] as const,
|
||||
}
|
||||
|
||||
export function lessonPlanItemGradeHistoryOptions(itemId: string) {
|
||||
return queryOptions({
|
||||
queryKey: lessonPlanItemKeys.gradeHistory(itemId),
|
||||
queryFn: () => api.get<LessonPlanItemGradeHistory[]>(`/v1/lesson-plan-items/${itemId}/grade-history`),
|
||||
enabled: !!itemId,
|
||||
})
|
||||
}
|
||||
|
||||
export const lessonPlanItemMutations = {
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<LessonPlanItem>(`/v1/lesson-plan-items/${id}`, data),
|
||||
addGrade: (id: string, data: Record<string, unknown>) =>
|
||||
api.post<{ record: LessonPlanItemGradeHistory; item: LessonPlanItem }>(`/v1/lesson-plan-items/${id}/grades`, data),
|
||||
}
|
||||
|
||||
// --- Lesson Plan Templates ---
|
||||
|
||||
export const lessonPlanTemplateKeys = {
|
||||
all: ['lesson-plan-templates'] as const,
|
||||
list: (params: PaginationInput) => [...lessonPlanTemplateKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...lessonPlanTemplateKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function lessonPlanTemplateListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: lessonPlanTemplateKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<LessonPlanTemplate>>('/v1/lesson-plan-templates', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function lessonPlanTemplateDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: lessonPlanTemplateKeys.detail(id),
|
||||
queryFn: () => api.get<LessonPlanTemplate>(`/v1/lesson-plan-templates/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const lessonPlanTemplateMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<LessonPlanTemplate>('/v1/lesson-plan-templates', data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<LessonPlanTemplate>(`/v1/lesson-plan-templates/${id}`, data),
|
||||
delete: (id: string) =>
|
||||
api.del<LessonPlanTemplate>(`/v1/lesson-plan-templates/${id}`),
|
||||
createPlan: (templateId: string, data: Record<string, unknown>) =>
|
||||
api.post<LessonPlan>(`/v1/lesson-plan-templates/${templateId}/create-plan`, data),
|
||||
}
|
||||
|
||||
// --- Store Closures ---
|
||||
|
||||
export const storeClosureKeys = {
|
||||
all: ['store-closures'] as const,
|
||||
}
|
||||
|
||||
export function storeClosureListOptions() {
|
||||
return queryOptions({
|
||||
queryKey: storeClosureKeys.all,
|
||||
queryFn: () => api.get<StoreClosure[]>('/v1/store-closures'),
|
||||
})
|
||||
}
|
||||
|
||||
export const storeClosureMutations = {
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post<StoreClosure>('/v1/store-closures', data),
|
||||
delete: (id: string) =>
|
||||
api.del<StoreClosure>(`/v1/store-closures/${id}`),
|
||||
}
|
||||
Reference in New Issue
Block a user