diff --git a/src/components/classes/edit/content/class-general-section.tsx b/src/components/classes/edit/content/class-general-section.tsx index 94b3abf1..86ec0af2 100644 --- a/src/components/classes/edit/content/class-general-section.tsx +++ b/src/components/classes/edit/content/class-general-section.tsx @@ -1,5 +1,8 @@ "use client"; +import { useEffect } from "react"; +import { useWatch } from "react-hook-form"; + import { CLASS_CATEGORIES } from "@/components/classes/constants"; import { FormInputField } from "@/components/form/FormInput"; import { FormSelectField } from "@/components/form/FormSelect"; @@ -22,9 +25,20 @@ import { FormError } from "@/components/form/FormLayout"; export function ClassGeneralSection() { const { - form: { control }, + form: { control, setValue, getValues }, } = useClassForm(); + const category = useWatch({ control, name: "category" }); + const isExercise = category?.includes("Exercise") ?? false; + + useEffect(() => { + if (isExercise && !getValues("levelRange")) { + setValue("levelRange", [1, 4], { shouldValidate: true, shouldDirty: true }); + } else if (!isExercise && getValues("levelRange")) { + setValue("levelRange", null, { shouldValidate: true, shouldDirty: true }); + } + }, [isExercise, setValue, getValues]); + return ( @@ -65,42 +79,45 @@ export function ClassGeneralSection() { /> - - {({ onChange, value, ...field }) => ( - <> - - - Levels - - - Level {value[0]} {value[0] !== value[1] && `- ${value[1]}`} - - - -
- - - {/* Labels for each level */} -
- Level 1 - Level 2 - Level 3 - Level 4 + {isExercise && ( + + {({ onChange, value, ...field }) => ( + <> + + + Levels + + + Level {value?.[0] ?? 1}{" "} + {value?.[0] !== value?.[1] && `- ${value?.[1] ?? 4}`} + + + +
+ + + {/* Labels for each level */} +
+ Level 1 + Level 2 + Level 3 + Level 4 +
-
- - - )} - + + + )} + + )} val.levelRange[0]! <= val.levelRange[1]!, { - error: "The upper level must be greater than the lower level", - path: ["levelRange"], - }); + .refine( + (val) => { + if (!val.levelRange) return true; + return val.levelRange[0]! <= val.levelRange[1]!; + }, + { + error: "The upper level must be greater than the lower level", + path: ["levelRange"], + }, + ) + .refine( + (val) => { + if (val.category.includes("Exercise")) { + return !!val.levelRange; + } + return true; + }, + { + error: "Levels are required for exercise classes", + path: ["levelRange"], + }, + ); export type ClassEditSchemaType = z.infer; export type ClassEditSchemaInput = z.input; export type ClassEditSchemaOutput = z.output; diff --git a/src/components/classes/edit/utils.ts b/src/components/classes/edit/utils.ts index 25c43661..41003e97 100644 --- a/src/components/classes/edit/utils.ts +++ b/src/components/classes/edit/utils.ts @@ -18,7 +18,7 @@ export function classToFormValues( category: c?.category ?? "", subcategory: c?.subcategory ?? "", image: (config ? buildImageUrl(config, c?.image) : undefined) ?? null, - levelRange: [c?.lowerLevel ?? 1, c?.upperLevel ?? 4], + levelRange: c?.lowerLevel && c?.upperLevel ? [c.lowerLevel, c.upperLevel] : null, schedules: c?.schedules.map((s) => ({ id: s.id, diff --git a/src/components/classes/list/components/class-card.tsx b/src/components/classes/list/components/class-card.tsx index 240e970f..c78133fe 100644 --- a/src/components/classes/list/components/class-card.tsx +++ b/src/components/classes/list/components/class-card.tsx @@ -62,15 +62,17 @@ export function ClassCard({ />
- - {classData.lowerLevel === classData.upperLevel ? ( - <>Level {classData.lowerLevel} - ) : ( - <> - Level {classData.lowerLevel}-{classData.upperLevel} - - )} - + {classData.lowerLevel && classData.upperLevel ? ( + + {classData.lowerLevel === classData.upperLevel ? ( + <>Level {classData.lowerLevel} + ) : ( + <> + Level {classData.lowerLevel}-{classData.upperLevel} + + )} + + ) : null} {classData.name} diff --git a/src/models/api/class.ts b/src/models/api/class.ts index 515f2394..fde4479b 100644 --- a/src/models/api/class.ts +++ b/src/models/api/class.ts @@ -12,8 +12,8 @@ export const CreateClass = z.object({ name: z.string().nonempty(), description: z.string().optional(), meetingURL : z.url().optional(), - lowerLevel: z.int().min(1).max(4), - upperLevel: z.int().min(1).max(4), + lowerLevel: z.int().min(1).max(4).nullish(), + upperLevel: z.int().min(1).max(4).nullish(), category: z.string(), subcategory: z.string().optional(), schedules: z.array(CreateSchedule).default([]), @@ -29,8 +29,8 @@ export const UpdateClass = z.object({ meetingURL: z.url().nullish(), category: z.string().optional(), subcategory: z.string().nullish(), - lowerLevel: z.int().optional(), - upperLevel: z.int().optional(), + lowerLevel: z.int().nullish(), + upperLevel: z.int().nullish(), addedSchedules: z.array(CreateSchedule).default([]), updatedSchedules: z.array(UpdateSchedule).default([]), deletedSchedules: z.array(z.uuid()).default([]), diff --git a/src/models/class.ts b/src/models/class.ts index 404e08c5..a22d6e2e 100644 --- a/src/models/class.ts +++ b/src/models/class.ts @@ -12,8 +12,8 @@ export type Class = { meetingURL?: string; category: string; subcategory?: string; - lowerLevel: number; - upperLevel: number; + lowerLevel: number | null; + upperLevel: number | null; schedules: Schedule[]; createdAt: Date; updatedAt: Date; diff --git a/src/server/db/migrations/0008_strong_saracen.sql b/src/server/db/migrations/0008_strong_saracen.sql new file mode 100644 index 00000000..123a82db --- /dev/null +++ b/src/server/db/migrations/0008_strong_saracen.sql @@ -0,0 +1,6 @@ +ALTER TABLE "course" DROP CONSTRAINT "chk_lower_level_bounds";--> statement-breakpoint +ALTER TABLE "course" DROP CONSTRAINT "chk_upper_level_bounds";--> statement-breakpoint +ALTER TABLE "course" ALTER COLUMN "lower_level" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "course" ALTER COLUMN "upper_level" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "course" ADD CONSTRAINT "chk_lower_level_bounds" CHECK ("course"."lower_level" IS NULL OR ("course"."lower_level" >= 1 AND "course"."lower_level" <= 4));--> statement-breakpoint +ALTER TABLE "course" ADD CONSTRAINT "chk_upper_level_bounds" CHECK ("course"."upper_level" IS NULL OR ("course"."upper_level" >= 1 AND "course"."upper_level" <= 4)); \ No newline at end of file diff --git a/src/server/db/migrations/meta/0008_snapshot.json b/src/server/db/migrations/meta/0008_snapshot.json new file mode 100644 index 00000000..454f2303 --- /dev/null +++ b/src/server/db/migrations/meta/0008_snapshot.json @@ -0,0 +1,2336 @@ +{ + "id": "a53aabbe-42ae-457e-bf90-a03842f5926e", + "prevId": "20873cbb-8f15-4573-9c32-696350ae2634", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_index": { + "name": "account_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_provider_id_account_id_index": { + "name": "account_provider_id_account_id_index", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appInvitation": { + "name": "appInvitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "domain_whitelist": { + "name": "domain_whitelist", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "appInvitation_inviter_id_index": { + "name": "appInvitation_inviter_id_index", + "columns": [ + { + "expression": "inviter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "appInvitation_email_index": { + "name": "appInvitation_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "appInvitation_status_index": { + "name": "appInvitation_status_index", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "appInvitation_inviter_id_user_id_fk": { + "name": "appInvitation_inviter_id_user_id_fk", + "tableFrom": "appInvitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_user_id_index": { + "name": "session_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_index": { + "name": "session_token_index", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_index": { + "name": "verification_identifier_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_identifier_value_index": { + "name": "verification_identifier_value_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blackout": { + "name": "blackout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "term_id": { + "name": "term_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "starts_on": { + "name": "starts_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "ends_on": { + "name": "ends_on", + "type": "date", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "blackout_term_id_starts_on_ends_on_index": { + "name": "blackout_term_id_starts_on_ends_on_index", + "columns": [ + { + "expression": "term_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_on", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ends_on", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blackout_schedule_id_starts_on_ends_on_index": { + "name": "blackout_schedule_id_starts_on_ends_on_index", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_on", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ends_on", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blackout_term_id_term_id_fk": { + "name": "blackout_term_id_term_id_fk", + "tableFrom": "blackout", + "tableTo": "term", + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blackout_schedule_id_schedule_id_fk": { + "name": "blackout_schedule_id_schedule_id_fk", + "tableFrom": "blackout", + "tableTo": "schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_blackout_owner_xor": { + "name": "chk_blackout_owner_xor", + "value": "( \"blackout\".\"term_id\" IS NOT NULL ) <> ( \"blackout\".\"schedule_id\" IS NOT NULL )" + }, + "chk_blackout_range_valid": { + "name": "chk_blackout_range_valid", + "value": "\"blackout\".\"ends_on\" >= \"blackout\".\"starts_on\"" + } + }, + "isRLSEnabled": false + }, + "public.course": { + "name": "course", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "term_id": { + "name": "term_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meeting_url": { + "name": "meeting_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lower_level": { + "name": "lower_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "upper_level": { + "name": "upper_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "course_term_id_index": { + "name": "course_term_id_index", + "columns": [ + { + "expression": "term_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "course_name_index": { + "name": "course_name_index", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "course_term_id_term_id_fk": { + "name": "course_term_id_term_id_fk", + "tableFrom": "course", + "tableTo": "term", + "columnsFrom": [ + "term_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_lower_level_bounds": { + "name": "chk_lower_level_bounds", + "value": "\"course\".\"lower_level\" IS NULL OR (\"course\".\"lower_level\" >= 1 AND \"course\".\"lower_level\" <= 4)" + }, + "chk_upper_level_bounds": { + "name": "chk_upper_level_bounds", + "value": "\"course\".\"upper_level\" IS NULL OR (\"course\".\"upper_level\" >= 1 AND \"course\".\"upper_level\" <= 4)" + } + }, + "isRLSEnabled": false + }, + "public.term": { + "name": "term", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "term_name": { + "name": "term_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "term_term_name_index": { + "name": "term_term_name_index", + "columns": [ + { + "expression": "term_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_term_date_ok": { + "name": "chk_term_date_ok", + "value": "\"term\".\"end_date\" >= \"term\".\"start_date\"" + } + }, + "isRLSEnabled": false + }, + "public.log": { + "name": "log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signoff": { + "name": "signoff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volunteer_user_id": { + "name": "volunteer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logs_volunteer": { + "name": "idx_logs_volunteer", + "columns": [ + { + "expression": "volunteer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_logs_course": { + "name": "idx_logs_course", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_logs_created_at": { + "name": "idx_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_logs_page": { + "name": "idx_logs_page", + "columns": [ + { + "expression": "page", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_volunteer_user_id_volunteer_user_id_fk": { + "name": "log_volunteer_user_id_volunteer_user_id_fk", + "tableFrom": "log", + "tableTo": "volunteer", + "columnsFrom": [ + "volunteer_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "log_course_id_course_id_fk": { + "name": "log_course_id_course_id_fk", + "tableFrom": "log", + "tableTo": "course", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instructor_to_schedule": { + "name": "instructor_to_schedule", + "schema": "", + "columns": { + "instructor_user_id": { + "name": "instructor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "instructor_to_schedule_instructor_user_id_index": { + "name": "instructor_to_schedule_instructor_user_id_index", + "columns": [ + { + "expression": "instructor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instructor_to_schedule_schedule_id_index": { + "name": "instructor_to_schedule_schedule_id_index", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "instructor_to_schedule_instructor_user_id_user_id_fk": { + "name": "instructor_to_schedule_instructor_user_id_user_id_fk", + "tableFrom": "instructor_to_schedule", + "tableTo": "user", + "columnsFrom": [ + "instructor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "instructor_to_schedule_schedule_id_schedule_id_fk": { + "name": "instructor_to_schedule_schedule_id_schedule_id_fk", + "tableFrom": "instructor_to_schedule", + "tableTo": "schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pk_instructor_schedule": { + "name": "pk_instructor_schedule", + "columns": [ + "instructor_user_id", + "schedule_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "effective_start": { + "name": "effective_start", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "effective_end": { + "name": "effective_end", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preferred_volunteer_count": { + "name": "preferred_volunteer_count", + "type": "smallint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "schedule_course_id_index": { + "name": "schedule_course_id_index", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_course_id_course_id_fk": { + "name": "schedule_course_id_course_id_fk", + "tableFrom": "schedule", + "tableTo": "course", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_schedule_duration_positive": { + "name": "chk_schedule_duration_positive", + "value": "\"schedule\".\"duration_minutes\" > 0" + }, + "chk_schedule_effective_range_valid": { + "name": "chk_schedule_effective_range_valid", + "value": "\"schedule\".\"effective_end\" IS NULL\n OR \"schedule\".\"effective_start\" IS NULL\n OR \"schedule\".\"effective_end\" >= \"schedule\".\"effective_start\"" + } + }, + "isRLSEnabled": false + }, + "public.volunteer_to_schedule": { + "name": "volunteer_to_schedule", + "schema": "", + "columns": { + "volunteer_user_id": { + "name": "volunteer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "volunteer_to_schedule_volunteer_user_id_index": { + "name": "volunteer_to_schedule_volunteer_user_id_index", + "columns": [ + { + "expression": "volunteer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volunteer_to_schedule_schedule_id_index": { + "name": "volunteer_to_schedule_schedule_id_index", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volunteer_to_schedule_volunteer_user_id_volunteer_user_id_fk": { + "name": "volunteer_to_schedule_volunteer_user_id_volunteer_user_id_fk", + "tableFrom": "volunteer_to_schedule", + "tableTo": "volunteer", + "columnsFrom": [ + "volunteer_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volunteer_to_schedule_schedule_id_schedule_id_fk": { + "name": "volunteer_to_schedule_schedule_id_schedule_id_fk", + "tableFrom": "volunteer_to_schedule", + "tableTo": "schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pk_volunteer_schedule": { + "name": "pk_volunteer_schedule", + "columns": [ + "volunteer_user_id", + "schedule_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coverage_request": { + "name": "coverage_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "shift_id": { + "name": "shift_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "coverage_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "coverage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "requesting_volunteer_user_id": { + "name": "requesting_volunteer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "covered_by_volunteer_user_id": { + "name": "covered_by_volunteer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "coverage_request_shift_id_requesting_volunteer_user_id_index": { + "name": "coverage_request_shift_id_requesting_volunteer_user_id_index", + "columns": [ + { + "expression": "shift_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_volunteer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "not \"coverage_request\".\"status\" = 'withdrawn'::coverage_status", + "concurrently": false, + "method": "btree", + "with": {} + }, + "coverage_request_shift_id_status_index": { + "name": "coverage_request_shift_id_status_index", + "columns": [ + { + "expression": "shift_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coverage_request_covered_by_volunteer_user_id_index": { + "name": "coverage_request_covered_by_volunteer_user_id_index", + "columns": [ + { + "expression": "covered_by_volunteer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coverage_request_requesting_volunteer_user_id_index": { + "name": "coverage_request_requesting_volunteer_user_id_index", + "columns": [ + { + "expression": "requesting_volunteer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coverage_request_shift_id_shift_id_fk": { + "name": "coverage_request_shift_id_shift_id_fk", + "tableFrom": "coverage_request", + "tableTo": "shift", + "columnsFrom": [ + "shift_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coverage_request_requesting_volunteer_user_id_volunteer_user_id_fk": { + "name": "coverage_request_requesting_volunteer_user_id_volunteer_user_id_fk", + "tableFrom": "coverage_request", + "tableTo": "volunteer", + "columnsFrom": [ + "requesting_volunteer_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coverage_request_covered_by_volunteer_user_id_volunteer_user_id_fk": { + "name": "coverage_request_covered_by_volunteer_user_id_volunteer_user_id_fk", + "tableFrom": "coverage_request", + "tableTo": "volunteer", + "columnsFrom": [ + "covered_by_volunteer_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shift": { + "name": "shift", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "class_id": { + "name": "class_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "canceled": { + "name": "canceled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancelled_by_user_id": { + "name": "cancelled_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shift_class_id_index": { + "name": "shift_class_id_index", + "columns": [ + { + "expression": "class_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shift_schedule_id_index": { + "name": "shift_schedule_id_index", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shift_date": { + "name": "idx_shift_date", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "class_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "not \"shift\".\"canceled\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shift_start": { + "name": "idx_shift_start", + "columns": [ + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "not \"shift\".\"canceled\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shift_slot": { + "name": "idx_shift_slot", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shift_class_id_course_id_fk": { + "name": "shift_class_id_course_id_fk", + "tableFrom": "shift", + "tableTo": "course", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shift_schedule_id_schedule_id_fk": { + "name": "shift_schedule_id_schedule_id_fk", + "tableFrom": "shift", + "tableTo": "schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shift_cancelled_by_user_id_user_id_fk": { + "name": "shift_cancelled_by_user_id_user_id_fk", + "tableFrom": "shift", + "tableTo": "user", + "columnsFrom": [ + "cancelled_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_shift_time": { + "name": "chk_shift_time", + "value": "\"shift\".\"end_at\" > \"shift\".\"start_at\"" + } + }, + "isRLSEnabled": false + }, + "public.shift_attendance": { + "name": "shift_attendance", + "schema": "", + "columns": { + "shift_id": { + "name": "shift_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "attendance_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "checked_in_at": { + "name": "checked_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "minutes_worked": { + "name": "minutes_worked", + "type": "smallint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shift_attendance_user_id_index": { + "name": "shift_attendance_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shift_attendance_shift_id_shift_id_fk": { + "name": "shift_attendance_shift_id_shift_id_fk", + "tableFrom": "shift_attendance", + "tableTo": "shift", + "columnsFrom": [ + "shift_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shift_attendance_user_id_volunteer_user_id_fk": { + "name": "shift_attendance_user_id_volunteer_user_id_fk", + "tableFrom": "shift_attendance", + "tableTo": "volunteer", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pk_shift_attendance": { + "name": "pk_shift_attendance", + "columns": [ + "shift_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.course_preference": { + "name": "course_preference", + "schema": "", + "columns": { + "volunteer_user_id": { + "name": "volunteer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "course_preference_volunteer_user_id_volunteer_user_id_fk": { + "name": "course_preference_volunteer_user_id_volunteer_user_id_fk", + "tableFrom": "course_preference", + "tableTo": "volunteer", + "columnsFrom": [ + "volunteer_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_preference_course_id_course_id_fk": { + "name": "course_preference_course_id_course_id_fk", + "tableFrom": "course_preference", + "tableTo": "course", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pk_course_preferences": { + "name": "pk_course_preferences", + "columns": [ + "volunteer_user_id", + "course_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_user_email": { + "name": "idx_user_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_role": { + "name": "idx_user_role", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_status": { + "name": "idx_user_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_created_at": { + "name": "idx_user_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volunteer": { + "name": "volunteer", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "preferred_name": { + "name": "preferred_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pronouns": { + "name": "pronouns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "province": { + "name": "province", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "bit(336)", + "primaryKey": false, + "notNull": true, + "default": "'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'" + }, + "preferred_time_commitment_hours": { + "name": "preferred_time_commitment_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_volunteer_city": { + "name": "idx_volunteer_city", + "columns": [ + { + "expression": "city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_volunteer_province": { + "name": "idx_volunteer_province", + "columns": [ + { + "expression": "province", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volunteer_user_id_user_id_fk": { + "name": "volunteer_user_id_user_id_fk", + "tableFrom": "volunteer", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.attendance_status": { + "name": "attendance_status", + "schema": "public", + "values": [ + "present", + "absent", + "excused", + "late" + ] + }, + "public.coverage_category": { + "name": "coverage_category", + "schema": "public", + "values": [ + "emergency", + "health", + "conflict", + "transportation", + "other" + ] + }, + "public.coverage_status": { + "name": "coverage_status", + "schema": "public", + "values": [ + "open", + "withdrawn", + "resolved" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "instructor", + "volunteer" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "unverified", + "rejected", + "active", + "inactive" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.vw_instructor_user": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"id\", \"name\", \"email\", \"email_verified\", \"image\", \"created_at\", \"updated_at\", \"role\", \"status\", \"last_name\" from \"user\" where \"user\".\"role\" = 'instructor'", + "name": "vw_instructor_user", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vw_volunteer_user": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "preferred_name": { + "name": "preferred_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pronouns": { + "name": "pronouns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "province": { + "name": "province", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "bit(336)", + "primaryKey": false, + "notNull": true, + "default": "'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'" + }, + "preferred_time_commitment_hours": { + "name": "preferred_time_commitment_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"user\".\"id\", \"user\".\"name\", \"user\".\"last_name\", \"user\".\"email\", \"user\".\"status\", \"user\".\"created_at\", \"user\".\"updated_at\", \"user\".\"email_verified\", \"user\".\"image\", \"user\".\"role\", \"volunteer\".\"preferred_name\", \"volunteer\".\"bio\", \"volunteer\".\"pronouns\", \"volunteer\".\"phone_number\", \"volunteer\".\"city\", \"volunteer\".\"province\", \"volunteer\".\"availability\", \"volunteer\".\"preferred_time_commitment_hours\" from \"user\" inner join \"volunteer\" on \"volunteer\".\"user_id\" = \"user\".\"id\" where \"user\".\"role\" = 'volunteer'", + "name": "vw_volunteer_user", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index d5115b0d..c458d6e5 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1772219284463, "tag": "0007_expand_availability_bitstring", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1772509470898, + "tag": "0008_strong_saracen", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema/course.ts b/src/server/db/schema/course.ts index a347a5be..3ca9d000 100644 --- a/src/server/db/schema/course.ts +++ b/src/server/db/schema/course.ts @@ -84,8 +84,8 @@ export const course = pgTable( meetingURL: text("meeting_url"), category: text("category").notNull(), subcategory: text("subcategory"), - lowerLevel: integer("lower_level").notNull(), - upperLevel: integer("upper_level").notNull(), + lowerLevel: integer("lower_level"), + upperLevel: integer("upper_level"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), @@ -98,11 +98,11 @@ export const course = pgTable( index().on(table.name), check( "chk_lower_level_bounds", - sql`${table.lowerLevel} >= 1 AND ${table.lowerLevel} <= 4`, + sql`${table.lowerLevel} IS NULL OR (${table.lowerLevel} >= 1 AND ${table.lowerLevel} <= 4)`, ), check( "chk_upper_level_bounds", - sql`${table.upperLevel} >= 1 AND ${table.upperLevel} <= 4`, + sql`${table.upperLevel} IS NULL OR (${table.upperLevel} >= 1 AND ${table.upperLevel} <= 4)`, ), ], ); diff --git a/src/test/integration/class-service.test.ts b/src/test/integration/class-service.test.ts index 67aacc31..971ff472 100644 --- a/src/test/integration/class-service.test.ts +++ b/src/test/integration/class-service.test.ts @@ -116,6 +116,33 @@ describe("ClassService", () => { return { termId, classId }; } + describe("class creation", () => { + it("should allow creating a non-exercise class without levels", async () => { + const termId = await termService.createTerm({ + name: `Test Term ${randomUUID()}`, + startDate: "2026-06-01", + endDate: "2026-08-31", + holidays: [], + }); + createdTermIds.push(termId); + + const classId = await classService.createClass({ + termId, + name: `Non-Exercise Class ${randomUUID()}`, + lowerLevel: null, + upperLevel: null, + category: "Literacy", + schedules: [], + }); + createdClassIds.push(classId); + + const result = await classService.getClass(classId); + expect(result.id).toBe(classId); + expect(result.lowerLevel).toBeNull(); + expect(result.upperLevel).toBeNull(); + }); + }); + describe("term published visibility", () => { it("admin should see classes in unpublished terms via getClass", async () => { const { classId } = await createTermAndClass({ publishTerm: false });