diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index b0f51210..e25e8527 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -41,8 +41,8 @@ jobs: branch: $GITHUB_BASE_REF regex: "terraform/*" - - name: Setup node env - uses: actions/setup-node@v2.1.2 + - name: Setup Node.js + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} @@ -106,6 +106,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + cache: 'pip' + cache-dependency-path: '**/requirements.txt' + + - name: Install Lambda dependencies + run: | + cd ../modules/lambda/files/cronMailing + pip install -r requirements.txt -t . - name: Hashicorp Terraform Setup (wraps stdout for plan) uses: hashicorp/setup-terraform@v1 @@ -172,7 +184,7 @@ jobs: body: output }) - - name: Terraform plan status + - name: Terraform Plan error if: steps.plan.outcome == 'failure' run: | echo Check terraform plan diff --git a/.github/workflows/ci-push.yml b/.github/workflows/ci-push.yml index 1dc1dee4..80139064 100644 --- a/.github/workflows/ci-push.yml +++ b/.github/workflows/ci-push.yml @@ -33,8 +33,8 @@ jobs: exit 1 fi - - name: Setup node env - uses: actions/setup-node@v2.1.2 + - name: Setup Node.js + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} @@ -76,6 +76,18 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + cache: 'pip' + cache-dependency-path: '**/requirements.txt' + + - name: Install Lambda dependencies + run: | + cd ../modules/lambda/files/cronMailing + pip install -r requirements.txt -t . + - name: Hashicorp Terraform Setup (wraps stdout for plan) uses: hashicorp/setup-terraform@v1 diff --git a/.gitignore b/.gitignore index c95b026b..954bd98a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ yarn-error.log *.zip **/cronMailing/* !**/cronMailing/*.py +!**/cronMailing/requirements.txt diff --git a/README.md b/README.md index 8829fdc5..ec61991f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Developers: Jason Huang, Soha Khan, Cindy Wang, Brandon Wong, Victor Yun, Mahad ```bash . ├── .github -│ ├── workflows/ci.yml # Github workflow +│ ├── workflows # Github workflows │ └── pull_request_template.md # PR template │ ├── pages # Pages @@ -57,7 +57,11 @@ Developers: Jason Huang, Soha Khan, Cindy Wang, Brandon Wong, Victor Yun, Mahad │ └── index.tsx │ ├── prisma # Prisma ORM -│ └── schema.prisma # Prisma Schema +│ │── dev-seeds # seeding data for dev environment +│ │── migrations # migrations for production +│ │── schema.prisma # Prisma Schema +│ │── schema.sql # SQL Schema +│ └── seed.ts # utility to script dev environment │ ├── public │ ├── icons # Icons @@ -67,12 +71,24 @@ Developers: Jason Huang, Soha Khan, Cindy Wang, Brandon Wong, Victor Yun, Mahad │ ├── src # Frontend tools │ ├── components # Components -│ └── definitions # Chakra +│ ├── definitions # Chakra │ └── styles # CSS and Colours │ -├── types # Depdendencies types +├── terraform # Infrastructure as code for dev and prod +│ ├── environments # code separated by environments +│ └── modules # terraform modules for reuse +│ +├── types # Dependency types │ ├── utils # Utility functions +│ │── containers # unstated-next containers +│ │── enum # enum utils +│ │── hooks # SWR API hooks +│ │── mail # SES mailing templates +│ │── request # API request utils +│ │── session # Session and authorization utils +│ │── time # time and date utils +│ │── toast # Chakra UI Toast msg utils │ └── validation # Data/Input Validators │ ├── services # Third party services @@ -85,6 +101,7 @@ Developers: Jason Huang, Soha Khan, Cindy Wang, Brandon Wong, Victor Yun, Mahad # Misc individual files ├── .babelrc ├── .eslintignore +├── .env.sample # required env vars ├── .gitattributes ├── .gitignore ├── .prettierignore @@ -111,10 +128,10 @@ Reset your database on Heroku and then deploy your database schema run (one-time ```bash # Drop all tables from current Heroku postgres database -heroku pg:reset -a YOUR_APP_NAME +heroku pg:reset -a # Deploy schema.sql to Heroku postgres -heroku pg:psql -a YOUR_APP_NAME -f prisma/schema.sql +heroku pg:psql -a -f prisma/schema.sql # Regenerate Prisma schema and client # optional - `npx prisma introspect` @@ -148,10 +165,26 @@ yarn lint yarn fix ``` +## ✈️ Migration + +NOTE: Before applying your migrations a production environment, ensure the diff via `npx prisma db pull` and `npx prisma migrate status` lines up with the migrations to be applied. + +To migrate a database schema without losing data: + +1. change both the `schema.sql` and `schema.prisma` file as required +2. run `prisma migrate dev --name --create-only` (this will require a [shadow database](https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database/#cloud-hosted-shadow-databases-must-be-created-manually)) +3. after the migration is approved, run `npx prisma migrate deploy` to apply all new migrations + +Baseline environment: + +Baselining initializes a migration history for databases that contain data and cannot be reset - such as the production database. Baselining tells Prisma Migrate to assume that one or more migrations have already been applied. Run the following command to baseline for each of the required migration: `prisma migrate resolve --applied ` + +For more info, please reference: [Adding Prisma Migrate to an existing project](https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/add-prisma-migrate-to-a-project) + ## 🚢 Deployment Deployments occur automatically on push to main and staging branches through [Railway](https://docs.railway.app/). -## License +## 📝 License [MIT](LICENSE) diff --git a/models/User.ts b/models/User.ts index 8e1e821d..4b2739d6 100644 --- a/models/User.ts +++ b/models/User.ts @@ -11,6 +11,7 @@ export type ParentInput = { isLowIncome?: boolean; preferredLanguage: locale; proofOfIncomeLink?: string; + proofOfIncomeSubmittedAt?: Date; heardFrom?: heardFrom[]; heardFromOther?: string; createStudentInput?: CreateStudentInput; @@ -23,6 +24,7 @@ export type VolunteerInput = { criminalRecordCheckLink?: string; criminalCheckApproved?: boolean; criminalCheckExpired?: boolean; + criminalCheckSubmittedAt?: Date; addressLine1: string; postalCode: string; cityName: string; diff --git a/package.json b/package.json index ac816492..32e41b73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "social-diversity-for-children", - "version": "0.1.0", + "version": "0.0.3", "private": true, "scripts": { "dev": "next dev", @@ -18,9 +18,6 @@ "fix:prettier": "prettier --write '**/*.{js,jsx,ts,tsx}'", "fix:eslint": "eslint '**/*.{js,jsx,ts,tsx}' --format stylish --fix" }, - "prisma": { - "seed": "ts-node -O {\"module\":\"CommonJS\"} prisma/seed.ts" - }, "dependencies": { "@chakra-ui/icons": "^1.0.14", "@chakra-ui/react": "^1.1.4", diff --git a/pages/admin/archive/index.tsx b/pages/admin/archive/index.tsx index 3bbd81ed..1a4a39c0 100644 --- a/pages/admin/archive/index.tsx +++ b/pages/admin/archive/index.tsx @@ -32,6 +32,7 @@ import { AdminLoading } from "@components/AdminLoading"; import { weekdayToString } from "@utils/enum/weekday"; import { ArchivedProgramClassInfoCard } from "@components/admin/ArchivedProgramClassInfoCard"; import { mutate } from "swr"; +import convertCamelToText from "@utils/convertCamelToText"; type ArchiveBrowseProgramsProps = { session: Session; @@ -68,6 +69,7 @@ export const ArchiveBrowsePrograms: React.FC = (prop } else if ( prog.name.toLowerCase().includes(term) || prog.description.toLowerCase().includes(term) || + convertCamelToText(prog.onlineFormat).toLowerCase().includes(term) || prog.onlineFormat.toLowerCase().includes(term) || prog.tag.toLowerCase().includes(term) ) { diff --git a/pages/admin/class/[id].tsx b/pages/admin/class/[id].tsx index bcaef006..be958cc9 100644 --- a/pages/admin/class/[id].tsx +++ b/pages/admin/class/[id].tsx @@ -27,6 +27,7 @@ import { AdminLoading } from "@components/AdminLoading"; import { Session } from "next-auth"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { AdminHeader } from "@components/admin/AdminHeader"; +import { roles } from "@prisma/client"; type ClassViewProps = { session: Session; @@ -69,7 +70,9 @@ export default function ClassView({ session }: ClassViewProps): JSX.Element { return ( - Programs + + Classes + "}> diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index d98be5f9..5e5f0b16 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -23,6 +23,7 @@ import Link from "next/link"; import React from "react"; import { MdClass, MdCreate, MdPersonAdd } from "react-icons/md"; import { RiCouponFill } from "react-icons/ri"; +import { roles } from "@prisma/client"; type AdminProps = { session: Session; @@ -53,29 +54,31 @@ export default function Admin(props: AdminProps): JSX.Element { Dashboard - - - - - - + {props.session?.role !== roles.TEACHER ? ( + + + + + + + ) : null} Overview and Analytics diff --git a/pages/admin/program/[pid].tsx b/pages/admin/program/[pid].tsx index f99c9a87..a384fca7 100644 --- a/pages/admin/program/[pid].tsx +++ b/pages/admin/program/[pid].tsx @@ -28,6 +28,7 @@ import { Session } from "next-auth"; import { isInternal } from "@utils/session/authorization"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { AdminHeader } from "@components/admin/AdminHeader"; +import { roles } from "@prisma/client"; type ClassViewProps = { session: Session; @@ -81,7 +82,9 @@ export default function ProgramClassView({ session }: ClassViewProps): JSX.Eleme return ( - Programs + + Programs + "}> diff --git a/pages/admin/program/index.tsx b/pages/admin/program/index.tsx index c8eb48ba..a0877023 100644 --- a/pages/admin/program/index.tsx +++ b/pages/admin/program/index.tsx @@ -16,6 +16,8 @@ import { Session } from "next-auth"; import { AdminLoading } from "@components/AdminLoading"; import { AdminError } from "@components/AdminError"; import { isInternal } from "@utils/session/authorization"; +import { roles } from "@prisma/client"; +import convertCamelToText from "@utils/convertCamelToText"; type BrowseProgramsProps = { session: Session; @@ -45,6 +47,7 @@ export const BrowsePrograms: React.FC = (props) => { } else if ( prog.name.toLowerCase().includes(term) || prog.description.toLowerCase().includes(term) || + convertCamelToText(prog.onlineFormat).toLowerCase().includes(term) || prog.onlineFormat.toLowerCase().includes(term) || prog.tag.toLowerCase().includes(term) ) { @@ -53,7 +56,9 @@ export const BrowsePrograms: React.FC = (props) => { }); return ( - Programs + + Programs + Browse Programs diff --git a/pages/admin/registrant/user/[id].tsx b/pages/admin/registrant/user/[id].tsx index 44bcb039..f1fc978e 100644 --- a/pages/admin/registrant/user/[id].tsx +++ b/pages/admin/registrant/user/[id].tsx @@ -33,6 +33,7 @@ import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { MdDescription, MdPerson, MdSupervisorAccount } from "react-icons/md"; import { mutate } from "swr"; +import checkExpiry from "@utils/checkExpiry"; type AdminProps = { session: Session; @@ -173,8 +174,7 @@ export default function Registrant(props: AdminProps): JSX.Element { /> ) : ( - The participant has not uploaded a criminal record check at this - time. + The participant has not uploaded a proof of income at this time. ), }); @@ -206,7 +206,12 @@ export default function Registrant(props: AdminProps): JSX.Element { header: "Criminal Record Check", canEdit: false, component: - user.volunteer.criminalRecordCheckLink !== null ? ( + user.volunteer.criminalRecordCheckLink !== null && + checkExpiry(user.volunteer.criminalCheckSubmittedAt) ? ( + + The participant's criminal record check is expired. + + ) : user.volunteer.criminalRecordCheckLink !== null ? ( `Class Reminder: ${name} ${weekdayToString( weekday, language, - )} ${convertToShortTimeRange(startTimeMinutes, durationMinutes)}`; + )} ${convertToShortTimeRange(totalMinutes(startDate), durationMinutes)}`; (parentRegs as any[]).forEach((reg) => { mailerPromises.push( diff --git a/pages/document-upload.tsx b/pages/document-upload.tsx index 67601363..02f07dc9 100644 --- a/pages/document-upload.tsx +++ b/pages/document-upload.tsx @@ -4,6 +4,7 @@ import { CloseButton } from "@components/CloseButton"; import DragAndDrop from "@components/DragAndDrop"; import { ApprovedIcon } from "@components/icons"; import Wrapper from "@components/SDCWrapper"; +import { pathWithQuery } from "@utils/request/query"; import { GetServerSideProps } from "next"; // Get server side props import { Session } from "next-auth"; import { getSession } from "next-auth/client"; @@ -30,6 +31,7 @@ export default function DocumentUpload({ session }: DocumentUploadProps): JSX.El // allows future support of multiple file uploads // if we really want to restrict to one document, remove the multiple from the input const [files, setFiles] = useState([]); + const [fileName, setFileName] = useState(); const upload = async () => { setIsUploading(true); @@ -50,6 +52,7 @@ export default function DocumentUpload({ session }: DocumentUploadProps): JSX.El if (fileUpload.ok) { console.log("Uploaded successfully!"); + setFileName(file.name); setUploadSuccess(true); } else { // TODO @@ -146,7 +149,13 @@ export default function DocumentUpload({ session }: DocumentUploadProps): JSX.El return ( - +
@@ -187,7 +196,15 @@ export default function DocumentUpload({ session }: DocumentUploadProps): JSX.El mt="20px" onClick={() => router - .push(redirect ? (redirect as string) : "/") + .push( + redirect + ? pathWithQuery( + redirect as string, + "uploaded", + fileName, + ) + : "/", + ) .then(() => { window.scrollTo({ top: 0 }); }) @@ -247,6 +264,7 @@ export default function DocumentUpload({ session }: DocumentUploadProps): JSX.El ); }; + if (uploadSuccess) { return uploadSuccessUI(); } else { diff --git a/pages/myaccounts.tsx b/pages/myaccounts.tsx index 5c31b599..3f5d3395 100644 --- a/pages/myaccounts.tsx +++ b/pages/myaccounts.tsx @@ -159,7 +159,7 @@ export default function MyAccount({ session }: MyAccountProps): JSX.Element { component: ( ), @@ -193,7 +193,7 @@ export default function MyAccount({ session }: MyAccountProps): JSX.Element { component: ( ), diff --git a/pages/parent/signup.tsx b/pages/parent/signup.tsx index 1c91a42e..dee509eb 100644 --- a/pages/parent/signup.tsx +++ b/pages/parent/signup.tsx @@ -89,7 +89,7 @@ const FormPage = (props) => { */ export default function ParticipantInfo({ session }: { session: Session }): JSX.Element { const router = useRouter(); - const { page } = router.query; + const { page, uploaded } = router.query; const { t } = useTranslation("form"); const [progressBar, setProgressBar] = useState(Number); const [pageNum, setPageNum] = useState(page ? parseInt(page as string, 10) : 0); @@ -417,9 +417,10 @@ export default function ParticipantInfo({ session }: { session: Session }): JSX. const parentData: ParentInput = { phoneNumber: parentPhoneNumber, - isLowIncome: undefined, // TODO + isLowIncome: undefined, preferredLanguage: locale.en, - proofOfIncomeLink: undefined, // TODO + proofOfIncomeLink: uploaded ? (uploaded as string) : undefined, + proofOfIncomeSubmittedAt: uploaded ? new Date() : undefined, heardFrom: [], createStudentInput: { firstName: participantFirstName, diff --git a/pages/volunteer/enrollment.tsx b/pages/volunteer/enrollment.tsx index 008ef4ff..deea6f9d 100644 --- a/pages/volunteer/enrollment.tsx +++ b/pages/volunteer/enrollment.tsx @@ -8,6 +8,7 @@ import { UpdateCriminalCheckForm } from "@components/volunteer-enroll/UpdateCrim import { VolunteerEnrolledFormWrapper } from "@components/volunteer-enroll/VolunteerEnrollFormWrapper"; import { locale } from "@prisma/client"; import CardInfoUtil from "@utils/cardInfoUtil"; +import checkExpiry from "@utils/checkExpiry"; import { fetcherWithQuery } from "@utils/fetcher"; import useMe from "@utils/hooks/useMe"; import useVolunteerRegistrations from "@utils/hooks/useVolunteerRegistration"; @@ -111,7 +112,7 @@ export const VolunteerEnrollment: React.FC = ({ // render update criminal check form if expired !me.volunteer.criminalRecordCheckLink ? pageElements.unshift() - : me.volunteer.criminalCheckExpired + : checkExpiry(me.volunteer.criminalCheckSubmittedAt) ? pageElements.unshift() : {}; diff --git a/pages/volunteer/signup.tsx b/pages/volunteer/signup.tsx index 1a7567a4..f00e5bb6 100644 --- a/pages/volunteer/signup.tsx +++ b/pages/volunteer/signup.tsx @@ -82,7 +82,7 @@ const FormPage = (props) => { */ export default function VolunteerInfo({ session }: { session: Session }): JSX.Element { const router = useRouter(); - const { page } = router.query; + const { page, uploaded } = router.query; const { t } = useTranslation("form"); const [progressBar, setProgressBar] = useState(Number); const [pageNum, setPageNum] = useState(page ? parseInt(page as string, 10) : 0); @@ -217,7 +217,8 @@ export default function VolunteerInfo({ session }: { session: Session }): JSX.El const volunteerData: VolunteerInput = { dateOfBirth: new Date(dateOfBirth), phoneNumber: phoneNumber, - criminalRecordCheckLink: undefined, + criminalRecordCheckLink: uploaded ? (uploaded as string) : undefined, + criminalCheckSubmittedAt: uploaded ? new Date() : undefined, addressLine1: address1, postalCode: postalCode, cityName: city, diff --git a/prisma/dev-seeds/class.ts b/prisma/dev-seeds/class.ts index 0ab8e6a0..e4cad90f 100644 --- a/prisma/dev-seeds/class.ts +++ b/prisma/dev-seeds/class.ts @@ -17,7 +17,6 @@ const classes: Class[] = [ startDate: new Date(), endDate: new Date(new Date().setDate(new Date().getDate() + 31)), weekday: weekday.WED, - startTimeMinutes: 1080, durationMinutes: 60, createdAt: new Date(), updatedAt: null, @@ -36,7 +35,6 @@ const classes: Class[] = [ startDate: new Date(), endDate: new Date(new Date().setDate(new Date().getDate() + 31)), weekday: weekday.THU, - startTimeMinutes: 1080, durationMinutes: 60, createdAt: new Date(), updatedAt: null, @@ -55,7 +53,6 @@ const classes: Class[] = [ startDate: new Date(), endDate: new Date(new Date().setDate(new Date().getDate() + 31)), weekday: weekday.FRI, - startTimeMinutes: 1020, durationMinutes: 60, createdAt: new Date(), updatedAt: null, diff --git a/prisma/migrations/20211229200913_init/migration.sql b/prisma/migrations/20211229200913_init/migration.sql new file mode 100644 index 00000000..bfdb83b5 --- /dev/null +++ b/prisma/migrations/20211229200913_init/migration.sql @@ -0,0 +1,299 @@ +-- CreateEnum +CREATE TYPE "locales" AS ENUM ('zh', 'en', 'ja', 'ko'); + +-- CreateEnum +CREATE TYPE "provinces" AS ENUM ('NL', 'PE', 'NS', 'NB', 'QC', 'ON', 'MB', 'SK', 'AB', 'BC', 'YT', 'NT', 'NU'); + +-- CreateEnum +CREATE TYPE "weekdays" AS ENUM ('MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'); + +-- CreateEnum +CREATE TYPE "program_formats" AS ENUM ('online', 'in-person', 'blended'); + +-- CreateEnum +CREATE TYPE "roles" AS ENUM ('PARENT', 'PROGRAM_ADMIN', 'TEACHER', 'VOLUNTEER'); + +-- CreateEnum +CREATE TYPE "difficulties" AS ENUM ('LEARNING', 'PHYSICAL', 'SENSORY', 'OTHER'); + +-- CreateEnum +CREATE TYPE "heard_from" AS ENUM ('FRIENDS_FAMILY', 'FLYERS', 'EMAIL', 'SOCIAL_MEDIA', 'OTHER'); + +-- CreateEnum +CREATE TYPE "therapy" AS ENUM ('PHYSIO', 'SPEECH_LANG', 'OCCUPATIONAL', 'COUNSELING', 'ART', 'OTHER'); + +-- CreateTable +CREATE TABLE "parents" ( + "id" SERIAL NOT NULL, + "phone_number" VARCHAR(50) NOT NULL, + "is_low_income" BOOLEAN, + "preferred_language" "locales" NOT NULL, + "proof_of_income_link" TEXT, + "proof_of_income_submitted_at" TIMESTAMPTZ(6), + "heard_from" "heard_from"[], + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "parents_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "programs" ( + "id" SERIAL NOT NULL, + "online_format" "program_formats" NOT NULL, + "tag" TEXT NOT NULL, + "image_link" TEXT, + "start_date" TIMESTAMPTZ(6) NOT NULL, + "end_date" TIMESTAMPTZ(6) NOT NULL, + "is_archived" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "programs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "program_translations" ( + "program_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "language" "locales" NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "program_translations_pkey" PRIMARY KEY ("program_id","language") +); + +-- CreateTable +CREATE TABLE "teachers" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "teachers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "teacher_regs" ( + "teacher_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "teacher_regs_pkey" PRIMARY KEY ("class_id","teacher_id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "first_name" TEXT, + "last_name" TEXT, + "email" TEXT, + "email_verified" TIMESTAMPTZ(6), + "role" "roles", + "image" TEXT, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "verification_requests" ( + "id" SERIAL NOT NULL, + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMPTZ(6) NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "verification_requests_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "volunteers" ( + "id" SERIAL NOT NULL, + "phone_number" VARCHAR(50), + "date_of_birth" TIMESTAMPTZ(6) NOT NULL, + "address_line1" TEXT, + "criminal_record_check_link" TEXT, + "criminal_check_approved" BOOLEAN, + "criminal_check_expired" BOOLEAN DEFAULT false, + "criminal_check_submitted_at" TIMESTAMPTZ(6), + "postal_code" VARCHAR(10), + "city_name" TEXT, + "province" "provinces", + "school" TEXT, + "preferred_language" "locales", + "skills" TEXT, + "hear_about_us" TEXT, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "volunteers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "volunteer_regs" ( + "volunteer_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "volunteer_regs_pkey" PRIMARY KEY ("volunteer_id","class_id") +); + +-- CreateTable +CREATE TABLE "program_admins" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "program_admins_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "class_translations" ( + "class_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "language" "locales" NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "class_translations_pkey" PRIMARY KEY ("class_id","language") +); + +-- CreateTable +CREATE TABLE "classes" ( + "id" SERIAL NOT NULL, + "name" TEXT, + "border_age" INTEGER NOT NULL, + "is_age_minimal" BOOLEAN NOT NULL DEFAULT false, + "image_link" TEXT, + "program_id" INTEGER NOT NULL, + "stripe_price_id" VARCHAR(50) NOT NULL, + "space_total" INTEGER NOT NULL, + "volunteer_space_total" INTEGER NOT NULL, + "is_archived" BOOLEAN NOT NULL DEFAULT false, + "start_date" TIMESTAMPTZ(6) NOT NULL, + "end_date" TIMESTAMPTZ(6) NOT NULL, + "weekday" "weekdays" NOT NULL, + "start_time_minutes" INTEGER NOT NULL, + "duration_minutes" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "classes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "waitlists" ( + "class_id" INTEGER NOT NULL, + "parent_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "waitlists_pkey" PRIMARY KEY ("parent_id","class_id") +); + +-- CreateTable +CREATE TABLE "parent_regs" ( + "parent_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "parent_regs_pkey" PRIMARY KEY ("parent_id","student_id","class_id") +); + +-- CreateTable +CREATE TABLE "students" ( + "id" SERIAL NOT NULL, + "parent_id" INTEGER NOT NULL, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "date_of_birth" TIMESTAMPTZ(6) NOT NULL, + "address_line1" TEXT NOT NULL, + "address_line2" TEXT, + "postal_code" VARCHAR(10), + "city_name" TEXT, + "province" "provinces", + "school" TEXT, + "grade" INTEGER, + "difficulties" "difficulties"[], + "other_difficulties" TEXT, + "therapy" "therapy"[], + "other_therapy" TEXT, + "special_education" BOOLEAN DEFAULT false, + "guardian_expectations" TEXT, + "medication" TEXT, + "allergies" TEXT, + "additional_info" TEXT, + "emerg_first_name" TEXT NOT NULL, + "emerg_last_name" TEXT NOT NULL, + "emerg_number" VARCHAR(50) NOT NULL, + "emerg_relation_to_student" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "students_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "verification_requests_token_key" ON "verification_requests"("token"); + +-- AddForeignKey +ALTER TABLE "parents" ADD CONSTRAINT "parents_id_fkey" FOREIGN KEY ("id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "program_translations" ADD CONSTRAINT "program_translations_program_id_fkey" FOREIGN KEY ("program_id") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "teachers" ADD CONSTRAINT "teachers_id_fkey" FOREIGN KEY ("id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "teacher_regs" ADD CONSTRAINT "teacher_regs_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "teacher_regs" ADD CONSTRAINT "teacher_regs_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "volunteers" ADD CONSTRAINT "volunteers_id_fkey" FOREIGN KEY ("id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "volunteer_regs" ADD CONSTRAINT "volunteer_regs_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "volunteer_regs" ADD CONSTRAINT "volunteer_regs_volunteer_id_fkey" FOREIGN KEY ("volunteer_id") REFERENCES "volunteers"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "program_admins" ADD CONSTRAINT "program_admins_id_fkey" FOREIGN KEY ("id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "class_translations" ADD CONSTRAINT "class_translations_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "classes" ADD CONSTRAINT "classes_program_id_fkey" FOREIGN KEY ("program_id") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "waitlists" ADD CONSTRAINT "waitlists_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "waitlists" ADD CONSTRAINT "waitlists_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "parent_regs" ADD CONSTRAINT "parent_regs_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "parent_regs" ADD CONSTRAINT "parent_regs_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "parent_regs" ADD CONSTRAINT "parent_regs_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "students" ADD CONSTRAINT "students_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/prisma/migrations/20211229201149_remove_starttime_minutes_field/migration.sql b/prisma/migrations/20211229201149_remove_starttime_minutes_field/migration.sql new file mode 100644 index 00000000..095f9e9e --- /dev/null +++ b/prisma/migrations/20211229201149_remove_starttime_minutes_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `start_time_minutes` on the `classes` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "classes" DROP COLUMN "start_time_minutes"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/prod-seeds/class-translation.ts b/prisma/prod-seeds/class-translation.ts deleted file mode 100644 index f24123c9..00000000 --- a/prisma/prod-seeds/class-translation.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { locale } from ".prisma/client"; -import { ClassTranslation } from "@prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed class translation data -const classTranslations: ClassTranslation[] = [ - { - classId: 10000, - name: "Singing Monkeys", - description: - "Children with special needs will be able to connect with the music teacher through an online video call to socialize and have fun while learning about music!", - language: locale.en, - createdAt: new Date(), - updatedAt: null, - }, - { - classId: 10000, - name: "會唱歌的猴子", - description: - "有特殊需要的儿童将能够通过线上视频教课跟音乐老师在交际和玩乐的环境下学习音乐!", - language: locale.zh, - createdAt: new Date(), - updatedAt: null, - }, - { - classId: 10000, - name: "Singing Monkeys", - description: "아이들은 영상 통화로 음악을 배우며 선생님과 대화하고 즐길 수 있습니다.", - language: locale.ko, - createdAt: new Date(), - updatedAt: null, - }, - { - classId: 10001, - name: "Singing Giraffes", - description: - "Children with special needs will be able to connect with the music teacher through an online video call to socialize and have fun while learning about music!", - language: locale.en, - createdAt: new Date(), - updatedAt: null, - }, - { - classId: 10001, - name: "會唱歌的长颈鹿", - description: - "有特殊需要的儿童将能够通过线上视频教课跟音乐老师在交际和玩乐的环境下学习音乐!", - language: locale.zh, - createdAt: new Date(), - updatedAt: null, - }, - { - classId: 10001, - name: "Singing Giraffes", - description: "아이들은 영상 통화로 음악을 배우며 선생님과 대화하고 즐길 수 있습니다.", - language: locale.ko, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert class translations - * @param data custom data to upsert - */ -export default async function classTranslationsUpsert(data?: ClassTranslation[]): Promise { - for (const translation of data || classTranslations) { - const { classId, language, createdAt, updatedAt, ...rest } = translation; - await prisma.classTranslation - .upsert({ - where: { - classId_language: { classId, language }, - }, - update: rest, - create: { - classId, - language, - createdAt, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/class.ts b/prisma/prod-seeds/class.ts deleted file mode 100644 index 264d9d9f..00000000 --- a/prisma/prod-seeds/class.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Class, weekday } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed class data -const classes: Class[] = [ - { - id: 10000, - name: "Singing Monkeys", - borderAge: 9, - isAgeMinimal: false, - imageLink: "https://i.imgur.com/2ZCdUW8.png", - programId: 10000, - stripePriceId: "price_1JmtVCL97YpjuvTOGSqYAdya", - spaceTotal: 10, - volunteerSpaceTotal: 5, - isArchived: false, - startDate: new Date(), - endDate: new Date(new Date().setDate(new Date().getDate() + 31)), - weekday: weekday.WED, - startTimeMinutes: 1080, - durationMinutes: 60, - createdAt: new Date(), - updatedAt: null, - }, - { - id: 10001, - name: "Singing Giraffes", - borderAge: 10, - isAgeMinimal: true, - imageLink: "https://i.imgur.com/Y4qw1al.png", - programId: 10000, - stripePriceId: "price_1JmtVpL97YpjuvTOaiyFxZqY", - spaceTotal: 10, - volunteerSpaceTotal: 5, - isArchived: false, - startDate: new Date(), - endDate: new Date(new Date().setDate(new Date().getDate() + 31)), - weekday: weekday.THU, - startTimeMinutes: 1080, - durationMinutes: 60, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert classes - * @param data custom data to upsert - */ -export default async function classUpsert(data?: Class[]): Promise { - for (const classRecord of data || classes) { - const { id, updatedAt, ...rest } = classRecord; - await prisma.class - .upsert({ - where: { - id, - }, - update: rest, - create: { - id, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/index.ts b/prisma/prod-seeds/index.ts deleted file mode 100644 index 3326be4c..00000000 --- a/prisma/prod-seeds/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import classUpsert from "./class"; -import classTranslationsUpsert from "./class-translation"; -import programUpsert from "./program"; -import programTranslationsUpsert from "./program-translation"; -import teacherUpsert from "./teacher"; -import teacherRegUpsert from "./teacher-reg"; -import userUpsert from "./user"; - -/** - * Seed the Production environment - */ -export default async function seedProd(): Promise { - await programUpsert(); - await programTranslationsUpsert(); - await classUpsert(); - await classTranslationsUpsert(); - await userUpsert(); - await teacherUpsert(); - await teacherRegUpsert(); -} diff --git a/prisma/prod-seeds/program-translation.ts b/prisma/prod-seeds/program-translation.ts deleted file mode 100644 index d22bc999..00000000 --- a/prisma/prod-seeds/program-translation.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { locale, ProgramTranslation } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed program translation data -const programTranslations: ProgramTranslation[] = [ - { - programId: 10000, - name: "Building Bridges with Music", - description: - "Building Bridges with Music is a music program where children with special needs will be able to connect with the music teacher through weekly class sessions to socialize and have fun while learning about music! This program will provide a safe environment for children to participate in interactive activities and build strong relationships during these unprecedented times! We will be offering both in-person and online options for this program in the Fall 2021 semester.", - language: locale.en, - createdAt: new Date(), - updatedAt: null, - }, - { - programId: 10000, - name: "音乐之桥", - description: - "有特殊需要的儿童将能够通过线上视频教课跟音乐老师在交际和玩乐的环境下学习音乐!", - language: locale.zh, - createdAt: new Date(), - updatedAt: null, - }, - { - programId: 10000, - name: "Building Bridges with Music", - description: "아이들은 영상 통화로 음악을 배우며 선생님과 대화하고 즐길 수 있습니다.", - language: locale.ko, - createdAt: new Date(), - updatedAt: null, - }, - { - programId: 10001, - name: "Education Through Creativity", - description: - "Education Through Creativity is an art program where children with special needs will be guided to develop their social skills and learn to communicate their thoughts and emotions through art. Led by an experienced art teacher, children will be able to build lasting friendships, learn more about expression, and partake in fun interactive activities while enjoying the beauty of art! We will be offering both in-person and online options for this program in the Fall 2021 semester.", - language: locale.en, - createdAt: new Date(), - updatedAt: null, - }, - { - programId: 10001, - name: "创意艺术", - description: - "有特殊需要的儿童将能够通过线上视频教课跟音乐老师在交际和玩乐的环境下学习音乐!", - language: locale.zh, - createdAt: new Date(), - updatedAt: null, - }, - { - programId: 10001, - name: "Education Through Creativity", - description: "아이들은 영상 통화로 음악을 배우며 선생님과 대화하고 즐길 수 있습니다.", - language: locale.ko, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert program translations - * @param data custom data to upsert - */ -export default async function programTranslationsUpsert( - data?: ProgramTranslation[], -): Promise { - for (const translation of data || programTranslations) { - const { programId, language, createdAt, updatedAt, ...rest } = translation; - await prisma.programTranslation - .upsert({ - where: { - programId_language: { programId, language }, - }, - update: rest, - create: { - programId, - language, - createdAt, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/program.ts b/prisma/prod-seeds/program.ts deleted file mode 100644 index 296336d9..00000000 --- a/prisma/prod-seeds/program.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Program, programFormat } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed program data -const programs: Program[] = [ - { - id: 10000, - onlineFormat: programFormat.online, - tag: "Music", - imageLink: - "https://images.squarespace-cdn.com/content/v1/5e83092341f99d6d384777ef/1608341017251-K0Q0U7BC37SQ5BGCV9G0/IMG_6646.jpg?format=750w", - startDate: new Date(), - endDate: new Date(new Date().setDate(new Date().getDate() + 7)), - isArchived: false, - createdAt: new Date(), - updatedAt: null, - }, - { - id: 10001, - onlineFormat: programFormat.online, - tag: "Art", - imageLink: - "https://images.squarespace-cdn.com/content/v1/5e83092341f99d6d384777ef/1608341093598-U3AGJCLP1UHUPZJNTYBY/IMG_2791.jpg?format=750w", - startDate: new Date(), - endDate: new Date(new Date().setDate(new Date().getDate() + 7)), - isArchived: false, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert programs - * @param data custom data to upsert - */ -export default async function programUpsert(data?: Program[]): Promise { - for (const program of data || programs) { - const { id, updatedAt, ...rest } = program; - await prisma.program - .upsert({ - where: { - id, - }, - update: rest, - create: { - id, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/teacher-reg.ts b/prisma/prod-seeds/teacher-reg.ts deleted file mode 100644 index 8a392379..00000000 --- a/prisma/prod-seeds/teacher-reg.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TeacherReg } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed teacher registrations data -const teacherRegs: TeacherReg[] = [ - { - teacherId: 10000, - classId: 10000, - createdAt: new Date(), - updatedAt: null, - }, - { - teacherId: 10000, - classId: 10001, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert teachers registrations - * @param data custom data to upsert - */ -export default async function teacherRegUpsert(data?: TeacherReg[]): Promise { - for (const teacherReg of data || teacherRegs) { - const { teacherId, classId, updatedAt, ...rest } = teacherReg; - await prisma.teacherReg - .upsert({ - where: { - classId_teacherId: { classId, teacherId }, - }, - update: rest, - create: { - teacherId, - classId, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/teacher.ts b/prisma/prod-seeds/teacher.ts deleted file mode 100644 index 8bae081f..00000000 --- a/prisma/prod-seeds/teacher.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Teacher } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// Seed teacher data -const teachers: Teacher[] = [ - { - id: 10000, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert teachers - * @param data custom data to upsert - */ -export default async function teacherUpsert(data?: Teacher[]): Promise { - for (const teacher of data || teachers) { - const { id, updatedAt, ...rest } = teacher; - await prisma.teacher - .upsert({ - where: { - id, - }, - update: rest, - create: { - id, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/prod-seeds/user.ts b/prisma/prod-seeds/user.ts deleted file mode 100644 index 5023789f..00000000 --- a/prisma/prod-seeds/user.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { roles, User } from ".prisma/client"; -import prisma from "../../services/database"; // Relative path required, aliases throw error using seed - -// TODO: Replace teachers with SDC internal users -// Seed user data -const users: User[] = [ - { - id: 10000, - firstName: "Brian", - lastName: "Anderson", - email: "ricksonyang+teacher@uwblueprint.org", - emailVerified: new Date(), - role: roles.TEACHER, - image: null, - createdAt: new Date(), - updatedAt: null, - }, -]; - -/** - * Upsert users - * @param data custom data to upsert - */ -export default async function userUpsert(data?: User[]): Promise { - for (const user of data || users) { - const { id, updatedAt, ...rest } = user; - await prisma.user - .upsert({ - where: { - id, - }, - update: rest, - create: { - id, - updatedAt, - ...rest, - }, - }) - .catch((err) => console.log(err)); - } -} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 32e962c2..8b5b0678 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,8 +3,10 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + // used for local only + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model Parent { @@ -13,7 +15,7 @@ model Parent { isLowIncome Boolean? @map("is_low_income") preferredLanguage locale @map("preferred_language") proofOfIncomeLink String? @map("proof_of_income_link") - proofOfIncomeSubmittedAt DateTime? @map("proof_of_income_submitted_at") @db.Timestamptz(6) + proofOfIncomeSubmittedAt DateTime? @map("proof_of_income_submitted_at") @db.Timestamptz(6) heardFrom heardFrom[] @map("heard_from") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) @@ -21,6 +23,7 @@ model Parent { parentRegs ParentReg[] @relation("parent_regsToparents") students Student[] @relation("parentsTostudents") waitlists Waitlist[] @relation("parentsTowaitlists") + @@map("parents") } @@ -36,6 +39,7 @@ model Program { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) classes Class[] @relation("programsToclasses") programTranslation ProgramTranslation[] @relation("program_translationsToprograms") + @@map("programs") } @@ -47,6 +51,7 @@ model ProgramTranslation { createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) program Program @relation("program_translationsToprograms", fields: [programId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([programId, language]) @@map("program_translations") } @@ -57,6 +62,7 @@ model Teacher { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) user User @relation("teachersTousers", fields: [id], references: [id], onDelete: Cascade, onUpdate: NoAction) teacherRegs TeacherReg[] @relation("teacher_regsToteachers") + @@map("teachers") } @@ -67,6 +73,7 @@ model TeacherReg { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) class Class @relation("classesToteacher_regs", fields: [classId], references: [id], onDelete: Cascade, onUpdate: NoAction) teacher Teacher @relation("teacher_regsToteachers", fields: [teacherId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([classId, teacherId]) @@map("teacher_regs") } @@ -85,6 +92,7 @@ model User { programAdmin ProgramAdmin? @relation("program_adminsTousers") teacher Teacher? @relation("teachersTousers") volunteer Volunteer? @relation("usersTovolunteers") + @@map("users") } @@ -95,6 +103,7 @@ model VerificationRequest { expires DateTime @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) + @@map("verification_requests") } @@ -129,6 +138,7 @@ model VolunteerReg { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) class Class @relation("classesTovolunteer_regs", fields: [classId], references: [id], onDelete: Cascade, onUpdate: NoAction) volunteer Volunteer @relation("volunteer_regsTovolunteers", fields: [volunteerId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([volunteerId, classId]) @@map("volunteer_regs") } @@ -138,6 +148,7 @@ model ProgramAdmin { createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) user User @relation("program_adminsTousers", fields: [id], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@map("program_admins") } @@ -149,6 +160,7 @@ model ClassTranslation { createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) class Class @relation(fields: [classId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([classId, language]) @@map("class_translations") } @@ -167,7 +179,6 @@ model Class { startDate DateTime @map("start_date") @db.Timestamptz(6) endDate DateTime @map("end_date") @db.Timestamptz(6) weekday weekday - startTimeMinutes Int @map("start_time_minutes") // @deprecated TODO: remove durationMinutes Int @map("duration_minutes") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) @@ -177,6 +188,7 @@ model Class { teacherRegs TeacherReg[] @relation("classesToteacher_regs") volunteerRegs VolunteerReg[] @relation("classesTovolunteer_regs") waitlists Waitlist[] + @@map("classes") } @@ -187,6 +199,7 @@ model Waitlist { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) class Class @relation(fields: [classId], references: [id], onDelete: Cascade, onUpdate: NoAction) parent Parent @relation("parentsTowaitlists", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([parentId, classId]) @@map("waitlists") } @@ -200,6 +213,7 @@ model ParentReg { class Class @relation("classesToparent_regs", fields: [classId], references: [id], onDelete: Cascade, onUpdate: NoAction) parent Parent @relation("parent_regsToparents", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: NoAction) student Student @relation(fields: [studentId], references: [id], onDelete: Cascade, onUpdate: NoAction) + @@id([parentId, studentId, classId]) @@map("parent_regs") } @@ -234,6 +248,7 @@ model Student { updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) parent Parent @relation("parentsTostudents", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: NoAction) parentRegs ParentReg[] + @@map("students") } @@ -242,6 +257,7 @@ enum locale { en ja ko + @@map("locales") } @@ -259,6 +275,7 @@ enum province { YT NT NU + @@map("provinces") } @@ -270,6 +287,7 @@ enum weekday { FRI SAT SUN + @@map("weekdays") } @@ -277,6 +295,7 @@ enum programFormat { online inPerson @map("in-person") blended + @@map("program_formats") } @@ -300,6 +319,7 @@ enum heardFrom { EMAIL SOCIAL_MEDIA OTHER + @@map("heard_from") } diff --git a/prisma/schema.sql b/prisma/schema.sql index 016f7b1a..e1085839 100644 --- a/prisma/schema.sql +++ b/prisma/schema.sql @@ -78,7 +78,6 @@ CREATE TABLE classes ( start_date TIMESTAMPTZ NOT NULL, end_date TIMESTAMPTZ NOT NULL, weekday weekdays NOT NULL, - start_time_minutes INTEGER NOT NULL, -- deprecated TODO: remove duration_minutes INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/prisma/seed.ts b/prisma/seed.ts index 36cd39e7..6a79e6f0 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,5 +1,4 @@ import seedDev from "./dev-seeds/index"; -import seedProd from "./prod-seeds/index"; import prisma from "../services/database"; /** @@ -7,8 +6,7 @@ import prisma from "../services/database"; */ const main = async () => { if (process.env.NODE_ENV === "production") { - console.log("Running production seed..."); - await seedProd(); + console.log("No production seed..."); } else { console.log("Running development seed..."); await seedDev(); diff --git a/public/locales/en/common.json b/public/locales/en/common.json index a3d03778..a0d96dab 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -54,13 +54,16 @@ "save": "Save Changes", "status_approved": "Status: Approved", "status_pending": "Status: Pending", + "status_expired": "Status: Expired", "status_declined": "Status: Declined", "dateSubmitted": "Date submitted: {{date}}", + "document": "Document", "poi_approved": "Your income statement has been approved. Please be on the lookout for an email from SDC with the discount for future registrations.", "poi_pending": "Your income statement is under review.", "poi_declined": "You have been declined for low income discounts. Please contact SDC if you think this is a mistake or submit a new income statement.", - "bgc_approved": "Your criminal record check has been approved.", + "bgc_approved": "Your criminal record check has been approved. It is valid for 1 year from date of submission.", "bgc_pending": "Your criminal record check is under review.", + "bgc_expired": "Your criminal record check is expired. Please submit a new criminal record check.", "bgc_declined": "Your criminal record check has been declined. Please contact SDC if you think this is a mistake or submit a new criminal record check.", "logOut": "Log out" }, diff --git a/public/locales/en/form.json b/public/locales/en/form.json index 01f879ba..ec28acab 100644 --- a/public/locales/en/form.json +++ b/public/locales/en/form.json @@ -27,7 +27,7 @@ "desc1": "As volunteering in our programs involves working closely with children and vulnerable persons, we ask that all our volunteers get a Criminal Record Check completed at their local RCMP office. The price of the CRC will be waived with a letter provided from SDC.", "desc2": "Also, please note that the MPM/JELIC is an IN-PERSON math program. If you apply to volunteer for this program, please ensure that you are aware that it is an in-person program and are able to attend the classes at Richmond Quantum Academy (6650-8181 Cambie Rd, Richmond, BC V6X 3X9).", "instruction": "Uploading your Criminal Record Check", - "instruction1": "Under My Account > Criminal Record Check - Generate a volunteer letter from SDC", + "instruction1": "Contact SDC for a volunteer criminal check letter", "instruction2": "Use the provided letter to obtain a criminal record check at the local police station or RCMP office", "instruction3": "Upload a copy of the result to your SDC account.", "instruction4": "Once you’ve submitted your letter, keep an eye out for approval status from SDC!", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json old mode 100644 new mode 100755 index 0eb0567b..d8301499 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -15,7 +15,9 @@ "ageGroupAbove": "{{age}}岁或以上", "teacherName": "{{name}}老师", "participantSpot": "剩余{{spot}}个参与者名额", + "participantSpot_plural": "{{spot}}個开放名额", "volunteerSpot": "剩余{{spot}}个义工名额", + "volunteerSpot_plural": "{{spot}}個 开放义工名额", "notEligible": "你的孩子不符合这个年龄组的条件。请选择一个不同的班级", "understood": "明白了", "waitlist": "添加到候补名单", @@ -28,11 +30,16 @@ "class": { "joinClass": "加入课程", "unregister": "取消注册", + "unregisterConfirm": "确认取消注册", + "unregisterInfo": "您确定要取消{{name}}的注册吗?", + "unregisterInfoAll": "您确定要取消所有学生的注册吗?", + "unregisterRefundInfo": "您需要联系 SDC 職員來取得退款。", "unregisterFor": "取消注册: {{name}}", "unregisterForAll": "全部注销", "upcomingClasses": "即将开始的课程", "waitlistedClasses": "候补课程", "waitlistedInfo": "当一个位置开放时,将向您发送一封电子邮件", + "waitlistCancel": "从候补名单中删除", "emptyWaitlist": "目前您没有被列入任何课程的候补名单。您候补名单上的任何课程都会显示在这里!", "emptyClass": "目前您还没有注册任何课程。您注册的任何课程都会显示在这里!" }, @@ -47,16 +54,21 @@ "save": "保存更改", "status_approved": "状态:批准", "status_pending": "状态:待定", + "status_expired": "状态:已过期", "status_declined": "状态:被拒", "dateSubmitted": "提交日期: {{date}}", "poi_approved": "您的收入报告已通过审核。", "poi_pending": "您的损益表正在审核中。", + "poi_declined": "您已被拒绝低收入折扣的申请。如果您认为这是一个错误或需要提交新的\n收入证明,请联系 SDC的職員", "bgc_approved": "您的无犯罪调查信已通过审核。", "bgc_pending": "您的无犯罪调查信正在被审查。", + "bgc_expired": "您的无犯罪记录已过期。请提交新的无犯罪记录", + "bgc_declined": "您的无犯罪记录已被拒绝。如果您认为这是一个错误或您想重新提交新的无犯罪记录,请联系 SDC的职员。", "logOut": "登出" }, "upload": { "title": "上传文档", + "prompt": "选择要上传的檔案", "instruction": "将您的文件拖放到此处", "alternative": "或者", "browseFiles": "浏览文件", @@ -70,7 +82,10 @@ "signIn": { "firstTime": "第一次参加吗?我们会给您发一个注册代码,让您立即注册。", "sentTitle": "已发送验证邮件。请检查您的电子邮件!", - "successEmail": "邮件认证成功!" + "sentInfo": "需要确认您的邮箱地址,请按我们发送给 {{email}} 的邮件中的链接。", + "sentHint": "還没有收到邮件?返回登录页面并重新输入有效的郵箱。", + "successEmail": "邮件认证成功!", + "successInfo": "您已确认 {{email}}。您可以开始填写您的帐户信息!" }, "time": { "range": "{{start}}至{{end}}", @@ -89,6 +104,7 @@ "therapy": { "physiotherapy": "物理疗法", "language": "语言治疗", + "occupational": "职业治疗", "psychotherapy": "辅导及心理治疗", "art": "音乐或艺术治疗", "other": "其他" @@ -101,6 +117,7 @@ "other": "其他" }, "nav": { + "signInNow": "立即登录去注册课程", "register": "注册", "registerNow": "立即注册", "browseProgram": "浏览课程", @@ -112,5 +129,16 @@ "browseClasses": "浏览课程", "viewDetails": "浏览详情", "viewAccount": "浏览账户" + }, + "toast": { + "registrationFailed": "注册失败", + "registrationFailedDesc": "该课程目前无法注册。", + "waitlistAdded": "添加候补名单", + "waitlistAddedDesc": "当有位置开放,您将收到一封邮件。" + }, + "404": { + "return": "返回首页", + "subtitle": "抱歉! 您要找的页面不存在, 请尝试刷新页面或点击下面的按钮。", + "title": "抱歉! 您要找的页面不存在。" } } diff --git a/public/locales/zh/form.json b/public/locales/zh/form.json old mode 100644 new mode 100755 index 0aabbde3..49946b9b --- a/public/locales/zh/form.json +++ b/public/locales/zh/form.json @@ -40,8 +40,11 @@ "form": { "accountCreated": "成功创建帐户", "accountCreatedInfo": "您的账户已成功创建。点击下面的按钮开始浏览课程!", + "volunteerCreatedInfo": "您的帐户已经注册成功。按下面的按钮可以开始浏览需要义工的课程!", "registered": "感謝您的註冊!", + "registeredInfo": "我们期待在我们的課程中见到您。您很快就会收到我们的电子邮件,其中包含更多信息!", "volunteerSignup": "感谢您报名成为义工!", + "volunteerSignupInfo": "我们真的很高兴你参与成为我们SDC义工。您很快就会收到我们的电子邮件,邮件里包含更多信息!", "skip": "暂时跳过", "next": "下一步", "finish": "完成", @@ -56,8 +59,21 @@ "selectChild": "您想要为谁注册", "confirmPersonalInformation": "确认个人信息", "hasChange": "以下信息有变化吗?请确认以下信息没有任何更改", + "updateInfo": "更新账户信息", "media": "媒体报道", + "media1": "如果超过 19 岁", + "media2": "我在此授权拍摄的任何图像或视频片段拍攝到我本人,全部或部分,单独或与其他图像和视频片段一起显示在藍絲帶基金会网站和藍絲帶基金会的其他官方渠道上或其合作伙伴、赞助商或附属实体,并用于媒体目的,包括促销演示、营销活动、纸质媒体、广播媒体、小册子、小册子、材料、书籍和所有其他途径。我还授权我在藍絲帶基金会内创作的任何媒体材料。", + "media3": "我放弃隐私权和补偿权,我可能因使用我的姓名和肖像而享有这些权利,包括获得与视频制作、编辑和推广相关的书面副本的权利。", + "media4": "如果未满 19 岁:", + "media5": "我在此授权我的孩子(18 岁以下)的任何图像或视频片段,全部或部分,单独或与其他图像和视频片段一起展示,在藍絲帶基金会和其他官方网站上展示藍絲帶基金会或其合作伙伴、赞助商或附属实体的渠道,并用于媒体目的,包括促销演示、营销活动、纸质媒体、广播媒体、小册子、小册子、材料、书籍和所有其他途径。我还授权在藍絲帶基金會内展示和使用我孩子创作的任何媒体材料。", + "media6": "我放弃隐私权和补偿权,我可能因使用我孩子的姓名和肖像而享有这些权利,包括获得与视频制作、编辑和推广相关的书面副本的权利。我已年满 19 岁,是參加者的父母或法定监护人,我已阅读本弃权书并熟悉其内容。", + "waiver": "參加者豁免", + "waiver1": "我在此完全同意并允许我的儿子/女儿参加藍絲帶基金会提供的課程。我知道我的孩子应该尊重其他參加者的情感安全和身体安全。如果孩子的行为不符合此标准,将通知家长/监护人。任何将其他参加者的安全置于危险之中的儿童可能会被要求退出課程。我同意让所有藍絲帶基金会的工作人员、承包商和義工,以及活动负责人,以及举办本次活动的任何一方,包括主任和總監,对因指定参与者参与已注册的課程而产生的任何责任不承担任何责任。", "conditions": "条款 & 条件", + "conditions1": "所有课程将 8 周(每周1节课),总计 $130(每节 $16.25 )。如果需要任何经济援助,政府有提供给特殊儿童的補貼來确保特殊儿童也有机会参与这些课程。请瀏覽sdcprograms.org 查看我们的财务档案并获取更多详细信息。", + "conditions2": "如果您没有获得任何基于政府的残疾批准,您有资格申请 SDC 的補助。如果申请成功,课程的费用會達到 $80/8次($10/次)。 SDC 補助金是由我们青年团队的努力提供,他们在任職期間努力筹款让更多的孩子受惠。", + "conditions3": "申请人将需要填写注册表上所列的所有表格才能获得 SDC 补贴。请查看sdcprograms.org 上我們接受的财务文件並了解有关 SDC 补贴和 SDC 提供的低收入补贴的更多信息。申请低收入补贴的申請者将被要求先支付 SDC 补贴费用($80),然后在确认審核成功后SDC會全款退还。", + "conditions4": "注册后,申請者将被要求预先付款,以确保參加者在課程中的位置。如果在第一次課程後並对繼續参加该課程有任何不确定,请参阅 sdcprograms.org 上的 SDC 的課程退款政策", "pay": "确认并支付", "redeemCoupon": "兑换优惠卷", "redeemInfo": "若要兑换优惠券,请在结帐时添加所需的优惠券。在提供支付前,将有一个添加优惠券代码的选项", @@ -82,6 +98,8 @@ "allergies": "您的孩子有任何食物过敏吗", "hearAboutUsTitle": "您是怎么听说我们的?", "hearAboutUs": "您是怎么听说我们的课程的?", + "certifyVolunteerAge": "我证明我已年满 15 岁,並可以在 SDC 担任义工", + "certifyVolunteerAttendance": "我保证我将承诺出席所有我报名参加的义工时段", "skillsAndExperience": "技能/经验", "hearAboutVolunteer": "您是怎么听说这次义工的机会的" }, @@ -96,11 +114,16 @@ "school": "学校", "grade": "年级", "difficulties": "参与者有", + "specialEducation": "在校的特殊教育", "therapy": "治疗种类", "guardianExpectations": "家长/监护人的期望", "guardianName": "家长/监护人姓名", + "phone": "电话", "relation": "与参与者的关系", "emergencyName": "紧急联系人姓名", + "emergencyFirstName": "紧急联系人名字", + "emergencyLastName": "紧急联系人的姓氏", + "emergencyContact": "紧急联系人", "emergencyPhone": "紧急联系人电话号码", "medication": "药物", "allergies": "食物过敏", @@ -111,6 +134,8 @@ "healthInformation": "健康信息", "email": "邮件", "details": "如有需要,请提供详细资料", + "volunteerInformation": "义工信息", + "volunteerPersonalDetails": "义工的个人资料", "firstName": "名", "lastName": "姓" } diff --git a/services/database/program-card-info.ts b/services/database/program-card-info.ts index c4ee46d1..f5edcb1c 100644 --- a/services/database/program-card-info.ts +++ b/services/database/program-card-info.ts @@ -96,7 +96,6 @@ async function getProgramCardInfoIncludeArchived(id: string, includeArchived = t return findResult; } - export { getProgramCardInfos, getProgramCardInfo, diff --git a/services/database/user.ts b/services/database/user.ts index 310cb7c3..f3e1d270 100644 --- a/services/database/user.ts +++ b/services/database/user.ts @@ -209,6 +209,7 @@ async function updateUser(userInput: UserInput) { isLowIncome: parentData.isLowIncome, preferredLanguage: parentData.preferredLanguage, proofOfIncomeLink: parentData.proofOfIncomeLink, + proofOfIncomeSubmittedAt: parentData.proofOfIncomeSubmittedAt, heardFrom: parentData.heardFrom, user: { connect: { id: user.id }, @@ -249,6 +250,7 @@ async function updateUser(userInput: UserInput) { phoneNumber: parentData.phoneNumber, isLowIncome: parentData.isLowIncome, proofOfIncomeLink: parentData.proofOfIncomeLink, + proofOfIncomeSubmittedAt: parentData.proofOfIncomeSubmittedAt, preferredLanguage: parentData.preferredLanguage, heardFrom: parentData.heardFrom, students: { @@ -340,6 +342,7 @@ async function updateUser(userInput: UserInput) { phoneNumber: volunteerData.phoneNumber, dateOfBirth: volunteerData.dateOfBirth, criminalRecordCheckLink: volunteerData.criminalRecordCheckLink, + criminalCheckSubmittedAt: volunteerData.criminalCheckSubmittedAt, addressLine1: volunteerData.addressLine1, postalCode: volunteerData.postalCode, cityName: volunteerData.cityName, @@ -356,6 +359,7 @@ async function updateUser(userInput: UserInput) { phoneNumber: volunteerData.phoneNumber, dateOfBirth: volunteerData.dateOfBirth, criminalRecordCheckLink: volunteerData.criminalRecordCheckLink, + criminalCheckSubmittedAt: volunteerData.criminalCheckSubmittedAt, addressLine1: volunteerData.addressLine1, postalCode: volunteerData.postalCode, cityName: volunteerData.cityName, diff --git a/src/components/ClassInfoModal.tsx b/src/components/ClassInfoModal.tsx index afed058d..0fe88d30 100644 --- a/src/components/ClassInfoModal.tsx +++ b/src/components/ClassInfoModal.tsx @@ -30,6 +30,7 @@ import { locale, roles } from "@prisma/client"; import { UseMeResponse } from "@utils/hooks/useMe"; import { infoToastOptions } from "@utils/toast/options"; import { totalMinutes } from "@utils/time/convert"; +import convertCamelToText from "@utils/convertCamelToText"; type ClassInfoModalProps = { isOpen: boolean; @@ -83,7 +84,7 @@ export const ClassInfoModal: React.FC = ({ })} - + {classInfo.description} diff --git a/src/components/MissingDocAlert.tsx b/src/components/MissingDocAlert.tsx index 90b6e902..e7bc9691 100644 --- a/src/components/MissingDocAlert.tsx +++ b/src/components/MissingDocAlert.tsx @@ -16,6 +16,7 @@ import { roles } from "@prisma/client"; import { UseMeResponse } from "@utils/hooks/useMe"; import Link from "next/link"; import { useTranslation } from "next-i18next"; +import checkExpiry from "@utils/checkExpiry"; type MissingDocAlertProps = { me?: UseMeResponse["me"]; @@ -28,6 +29,11 @@ export const MissingDocAlert: React.FC = ({ me }) => { const missingPOI = me && me.role === roles.PARENT && me.parent.proofOfIncomeLink === null; const missingCriminalCheck = me && me.role === roles.VOLUNTEER && me.volunteer.criminalRecordCheckLink === null; + const expiredCriminalCheck = + me && + me.role === roles.VOLUNTEER && + me.volunteer.criminalRecordCheckLink != null && + checkExpiry(me.volunteer.criminalCheckSubmittedAt); const InfoCaption = [ { @@ -38,11 +44,15 @@ export const MissingDocAlert: React.FC = ({ me }) => { heading: t("bgc.submitTitle"), desc: t("bgc.missing"), }, + { + heading: t("bgc.submitTitle"), + desc: t("bgc.expired"), + }, ]; return ( - {!read && (missingPOI || missingCriminalCheck) && ( + {!read && (missingPOI || missingCriminalCheck || expiredCriminalCheck) && ( = ({ me }) => { )} + {expiredCriminalCheck && ( + + + {InfoCaption[2].heading} + + + {InfoCaption[2].desc} + + + )} diff --git a/src/components/ProgramCard.tsx b/src/components/ProgramCard.tsx index 44c3d2b4..efd2bb56 100644 --- a/src/components/ProgramCard.tsx +++ b/src/components/ProgramCard.tsx @@ -9,6 +9,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useTranslation } from "react-i18next"; import { Session } from "next-auth"; +import convertCamelToText from "@utils/convertCamelToText"; type ProgramCardProps = { styleProps?: Record; @@ -110,7 +111,10 @@ export const ProgramCard: React.FC = ({ cardInfo }): JSX.Eleme - + diff --git a/src/components/ProgramInfo.tsx b/src/components/ProgramInfo.tsx index d002f00e..339c9475 100644 --- a/src/components/ProgramInfo.tsx +++ b/src/components/ProgramInfo.tsx @@ -27,6 +27,7 @@ import { EmptyState } from "./EmptyState"; import Participants from "@utils/containers/Participants"; import { UseMeResponse } from "@utils/hooks/useMe"; import { Session } from "next-auth"; +import convertCamelToText from "@utils/convertCamelToText"; /** * programInfo is the program information that will be displayed on the home page, follows the ProgramCardInfo type @@ -67,7 +68,7 @@ export const ProgramInfo: React.FC = ({ const programTags = ( - + ); diff --git a/src/components/admin/ArchivedBrowseProgramCard.tsx b/src/components/admin/ArchivedBrowseProgramCard.tsx index a3436fc4..9db39600 100644 --- a/src/components/admin/ArchivedBrowseProgramCard.tsx +++ b/src/components/admin/ArchivedBrowseProgramCard.tsx @@ -16,6 +16,7 @@ import { import { AdminBadge } from "@components/AdminBadge"; import { locale, roles } from "@prisma/client"; import colourTheme from "@styles/colours"; +import convertCamelToText from "@utils/convertCamelToText"; import convertToShortDateRange from "@utils/convertToShortDateRange"; import { deleteProgram } from "@utils/deleteProgram"; import { infoToastOptions } from "@utils/toast/options"; @@ -134,7 +135,7 @@ export const ArchivedBrowseProgramCard: React.FC {cardInfo.tag} - {cardInfo.onlineFormat} + {convertCamelToText(cardInfo.onlineFormat)} diff --git a/src/components/admin/ArchivedProgramViewInfoCard.tsx b/src/components/admin/ArchivedProgramViewInfoCard.tsx index 0050b045..10850a38 100644 --- a/src/components/admin/ArchivedProgramViewInfoCard.tsx +++ b/src/components/admin/ArchivedProgramViewInfoCard.tsx @@ -32,6 +32,7 @@ import { IoEllipsisVertical } from "react-icons/io5"; import { AdminModal } from "./AdminModal"; import { roles } from "@prisma/client"; import { infoToastOptions } from "@utils/toast/options"; +import convertCamelToText from "@utils/convertCamelToText"; export type ArchivedProgramViewInfoCard = { cardInfo: ProgramCardInfo; @@ -138,7 +139,9 @@ export const ArchivedProgramViewInfoCard: React.FC {cardInfo.tag} - {cardInfo.onlineFormat} + + {convertCamelToText(cardInfo.onlineFormat)} + diff --git a/src/components/admin/BrowseProgramCard.tsx b/src/components/admin/BrowseProgramCard.tsx index aeef85a6..5d1f77ec 100644 --- a/src/components/admin/BrowseProgramCard.tsx +++ b/src/components/admin/BrowseProgramCard.tsx @@ -16,6 +16,7 @@ import { import { AdminBadge } from "@components/AdminBadge"; import { locale, roles } from "@prisma/client"; import colourTheme from "@styles/colours"; +import convertCamelToText from "@utils/convertCamelToText"; import convertToShortDateRange from "@utils/convertToShortDateRange"; import { deleteProgram } from "@utils/deleteProgram"; import { infoToastOptions } from "@utils/toast/options"; @@ -129,7 +130,7 @@ export const BrowseProgramCard: React.FC = ({ {cardInfo.tag} - {cardInfo.onlineFormat} + {convertCamelToText(cardInfo.onlineFormat)} diff --git a/src/components/admin/ProgramViewInfoCard.tsx b/src/components/admin/ProgramViewInfoCard.tsx index cb13b704..670f522d 100644 --- a/src/components/admin/ProgramViewInfoCard.tsx +++ b/src/components/admin/ProgramViewInfoCard.tsx @@ -32,6 +32,7 @@ import { IoEllipsisVertical } from "react-icons/io5"; import { AdminModal } from "./AdminModal"; import { roles } from "@prisma/client"; import { infoToastOptions } from "@utils/toast/options"; +import convertCamelToText from "@utils/convertCamelToText"; export type ProgramViewInfoCard = { cardInfo: ProgramCardInfo; @@ -132,7 +133,9 @@ export const ProgramViewInfoCard: React.FC = ({ cardInfo, r {cardInfo.tag} - {cardInfo.onlineFormat} + + {convertCamelToText(cardInfo.onlineFormat)} + diff --git a/src/components/fileDownloadCard.tsx b/src/components/fileDownloadCard.tsx index 58a2f467..d19a8712 100644 --- a/src/components/fileDownloadCard.tsx +++ b/src/components/fileDownloadCard.tsx @@ -18,7 +18,6 @@ import colourTheme from "@styles/colours"; import { updateFileApproval } from "@utils/updateFileApproval"; import { FileType } from "@utils/enum/filetype"; import convertToShortDateString from "@utils/convertToShortDateString"; - type FileDownloadCardProps = { filePath: FileType; docName: string; diff --git a/src/components/formFields/DateField.tsx b/src/components/formFields/DateField.tsx index fdbc65b1..7cd766e7 100644 --- a/src/components/formFields/DateField.tsx +++ b/src/components/formFields/DateField.tsx @@ -38,6 +38,7 @@ export const DateField: React.FC = ({ }} > = ({ onChange={(date) => setValue(date)} showTimeSelect={time} timeFormat="HH:mm" + dropdownMode="select" /> )} diff --git a/src/components/myAccounts/CriminalCheck.tsx b/src/components/myAccounts/CriminalCheck.tsx index 96e44f1e..e11f5fdb 100644 --- a/src/components/myAccounts/CriminalCheck.tsx +++ b/src/components/myAccounts/CriminalCheck.tsx @@ -1,37 +1,48 @@ import React from "react"; -import { Box, Button, Flex, Text } from "@chakra-ui/react"; +import { Box, Button, Flex, Text, Link as ChakraLink } from "@chakra-ui/react"; import colourTheme from "@styles/colours"; import Link from "next/link"; import { ApprovedIcon, InfoIcon, PendingIcon } from "@components/icons"; import convertToShortDateString from "@utils/convertToShortDateString"; import { locale } from "@prisma/client"; import { useTranslation } from "next-i18next"; +import checkExpiry from "@utils/checkExpiry"; +import useFileRetrieve from "@utils/hooks/useFileRetrieve"; +import { FileType } from "@utils/enum/filetype"; type CriminalCheckProps = { - link: string; + file: string; approved: boolean; submitDate?: Date; }; export const CriminalCheck: React.FC = ({ - link, + file, approved, submitDate, }): JSX.Element => { const { t } = useTranslation(["form", "common"]); + const { url: docLink } = useFileRetrieve(FileType.CRIMINAL_CHECK, file); let description; let status; let icon; - if (approved) { + if (approved && checkExpiry(submitDate)) { + status = "expired"; + description = t("account.bgc", { + ns: "common", + context: status, + }); + icon = ; + } else if (approved) { status = "approved"; description = t("account.bgc", { ns: "common", context: status, }); icon = ; - } else if (link == null) { + } else if (file == null) { description = t("bgc.missing"); icon = ; } else if (approved === null) { @@ -61,7 +72,7 @@ export const CriminalCheck: React.FC = ({ - {link == null ? null : ( + {file == null ? null : ( <> {t("account.status", { @@ -75,6 +86,17 @@ export const CriminalCheck: React.FC = ({ date: convertToShortDateString(submitDate, locale.en, true), })} + + + {t("account.document", { + ns: "common", + })} + :{" "} + + + {file} + + )} diff --git a/src/components/myAccounts/ProofOfIncome.tsx b/src/components/myAccounts/ProofOfIncome.tsx index 654f072a..5fc359f0 100644 --- a/src/components/myAccounts/ProofOfIncome.tsx +++ b/src/components/myAccounts/ProofOfIncome.tsx @@ -1,24 +1,27 @@ import React from "react"; -import { Box, Button, Flex, Text } from "@chakra-ui/react"; +import { Box, Button, Flex, Text, Link as ChakraLink } from "@chakra-ui/react"; import colourTheme from "@styles/colours"; import Link from "next/link"; import { ApprovedIcon, InfoIcon, PendingIcon } from "@components/icons"; import convertToShortDateString from "@utils/convertToShortDateString"; import { locale } from "@prisma/client"; import { useTranslation } from "react-i18next"; +import useFileRetrieve from "@utils/hooks/useFileRetrieve"; +import { FileType } from "@utils/enum/filetype"; type ProofOfIncomeProps = { - link: string; + file: string; approved: boolean; submitDate?: Date; }; export const ProofOfIncome: React.FC = ({ - link, + file, approved, submitDate, }): JSX.Element => { const { t } = useTranslation(["form", "common"]); + const { url: docLink } = useFileRetrieve(FileType.INCOME_PROOF, file); let description; let status; @@ -31,7 +34,7 @@ export const ProofOfIncome: React.FC = ({ context: status, }); icon = ; - } else if (link == null) { + } else if (file == null) { description = t("poi.missing"); icon = ; } else if (approved === null) { @@ -61,7 +64,7 @@ export const ProofOfIncome: React.FC = ({ - {link == null ? null : ( + {file == null ? null : ( <> {t("account.status", { @@ -75,6 +78,17 @@ export const ProofOfIncome: React.FC = ({ date: convertToShortDateString(submitDate, locale.en, true), })} + + + {t("account.document", { + ns: "common", + })} + :{" "} + + + {file} + + )} diff --git a/src/components/parent-form/HeardFromPage.tsx b/src/components/parent-form/HeardFromPage.tsx index 6e8a6ace..2a6c618b 100644 --- a/src/components/parent-form/HeardFromPage.tsx +++ b/src/components/parent-form/HeardFromPage.tsx @@ -30,7 +30,6 @@ export const HeardFromPage: React.FC = ({ props }): JSX.Elem {t("signUp.hearAboutUs")} - {t("signUp.participantHaveDifficulties")} ; @@ -49,6 +50,7 @@ export const VolunteerDetailsPage: React.FC = ({ value={props.certifyAge15} name={t("signUp.certifyVolunteerAge")} setValue={props.setCertifyAge15} + edit={moment().diff(props.dateOfBirth, "years") >= 15} > 0) { + return false; + } + return true; +} diff --git a/utils/convertCamelToText.ts b/utils/convertCamelToText.ts new file mode 100644 index 00000000..127144fa --- /dev/null +++ b/utils/convertCamelToText.ts @@ -0,0 +1,10 @@ +/** + * Converts camel case string into normal text + * EG: "inPerson" -> "In Person" + * @param text Camel case text + * @returns the corresponding normal text with proper spacing + */ +export default function convertCamelToText(text: string): string { + const result = text.replace(/([A-Z])/g, " $1"); + return result.charAt(0).toUpperCase() + result.slice(1); +} diff --git a/utils/hooks/useVolunteerRegTableData.tsx b/utils/hooks/useVolunteerRegTableData.tsx index b4cf67d7..53301452 100644 --- a/utils/hooks/useVolunteerRegTableData.tsx +++ b/utils/hooks/useVolunteerRegTableData.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import React from "react"; import { Link as ChakraLink } from "@chakra-ui/react"; import { CellProps } from "react-table"; +import checkExpiry from "@utils/checkExpiry"; export type VolunteerDataType = { id: number; @@ -96,7 +97,9 @@ export default function useVolunteerRegTableData( : "N/A", age: convertToAge(new Date(reg.volunteer.dateOfBirth)), criminalCheckApproved: reg.volunteer.criminalCheckApproved - ? "Complete" + ? checkExpiry(reg.volunteer.criminalCheckSubmittedAt) + ? "Expired" + : "Complete" : reg.volunteer.criminalRecordCheckLink ? "Pending" : "Incomplete", diff --git a/utils/hooks/useVolunteersTableData.tsx b/utils/hooks/useVolunteersTableData.tsx index e20f0fa8..529d2016 100644 --- a/utils/hooks/useVolunteersTableData.tsx +++ b/utils/hooks/useVolunteersTableData.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import React from "react"; import { Link as ChakraLink } from "@chakra-ui/react"; import { CellProps } from "react-table"; +import checkExpiry from "@utils/checkExpiry"; export type VolunteerDataType = { id: number; @@ -94,7 +95,9 @@ export default function useVolunteersTableData( : "N/A", age: convertToAge(new Date(user.volunteer.dateOfBirth)), criminalCheckApproved: user.volunteer.criminalCheckApproved - ? "Complete" + ? checkExpiry(user.volunteer.criminalCheckSubmittedAt) + ? "Expired" + : "Complete" : user.volunteer.criminalRecordCheckLink ? "Pending" : "Incomplete", diff --git a/utils/validation/user.ts b/utils/validation/user.ts index 6115f2bb..a9571983 100644 --- a/utils/validation/user.ts +++ b/utils/validation/user.ts @@ -116,6 +116,11 @@ function getUserValidationErrors(user: UserInput): Array { if (!validatePreferredLanguage(roleData.preferredLanguage)) { validationErrors.push(`Invalid preferred language: ${roleData.preferredLanguage}`); } + if (roleData.proofOfIncomeLink && !roleData.proofOfIncomeSubmittedAt) { + validationErrors.push( + `Invalid submit date provided: ${roleData.proofOfIncomeSubmittedAt}`, + ); + } } else if (user.role === roles.PROGRAM_ADMIN) { // pass - since program admin has no unique fields } else if (user.role === roles.TEACHER) { @@ -136,6 +141,11 @@ function getUserValidationErrors(user: UserInput): Array { `Invalid preferred language provided: ${roleData.preferredLanguage}`, ); } + if (roleData.criminalRecordCheckLink && !roleData.criminalCheckSubmittedAt) { + validationErrors.push( + `Invalid submit date provided: ${roleData.criminalCheckSubmittedAt}`, + ); + } } return validationErrors;