diff --git a/apps/anvil/dbschema/README.md b/apps/anvil/dbschema/README.md index 5e1f5a9..18858d9 100644 --- a/apps/anvil/dbschema/README.md +++ b/apps/anvil/dbschema/README.md @@ -15,8 +15,11 @@ Regeneration: - `npx @edgedb/generate edgeql-js` - regenerate the query builder generator. - `pnpm edgedb-zod` - regenerates the Zod schema for the DB. + **These Must Be Extended If You Use `z.date` (use `z.string().datetime()`)** + If you want to run all of these at once, `pnpm regen`. ## Some comments 🥁 Inline comments are why things were done, the `annotation description := "..."` comments are specifically on schema and representation. + diff --git a/apps/anvil/dbschema/queries/addInPersonTraining.edgeql b/apps/anvil/dbschema/queries/addInPersonTraining.edgeql new file mode 100644 index 0000000..ef96b91 --- /dev/null +++ b/apps/anvil/dbschema/queries/addInPersonTraining.edgeql @@ -0,0 +1,19 @@ +with rep := ( + select users::Rep + filter .id = $rep_id +), +user := ( + update users::User + filter .id = $id + set { + training := ( + select .training { + @created_at := @created_at, + @in_person_created_at := $created_at, + @in_person_signed_off_by := assert_exists(rep.id), + } + filter .id = $training_id + ) + } +) +select assert_exists(user); diff --git a/apps/anvil/dbschema/queries/addInPersonTraining.query.ts b/apps/anvil/dbschema/queries/addInPersonTraining.query.ts new file mode 100644 index 0000000..4ba1923 --- /dev/null +++ b/apps/anvil/dbschema/queries/addInPersonTraining.query.ts @@ -0,0 +1,38 @@ +// GENERATED by @edgedb/generate v0.5.2 + +import type {Executor} from "edgedb"; + +export type AddInPersonTrainingArgs = { + readonly "rep_id": string; + readonly "training_id": string; + readonly "created_at": Date; + readonly "id": string; +}; + +export type AddInPersonTrainingReturns = { + "id": string; +}; + +export function addInPersonTraining(client: Executor, args: AddInPersonTrainingArgs): Promise { + return client.queryRequiredSingle(`\ +with rep := ( + select users::Rep + filter .id = $rep_id +), +user := ( + update users::User + filter .id = $id + set { + training := ( + select .training { + @created_at := @created_at, + @in_person_created_at := $created_at, + @in_person_signed_off_by := assert_exists(rep.id), + } + filter .id = $training_id + ) + } +) +select assert_exists(user);`, args); + +} diff --git a/apps/anvil/src/users/dto/users.dto.ts b/apps/anvil/src/users/dto/users.dto.ts index 1e9cad1..2af5977 100644 --- a/apps/anvil/src/users/dto/users.dto.ts +++ b/apps/anvil/src/users/dto/users.dto.ts @@ -1,14 +1,22 @@ -import { CreateInfractionSchema, CreateUserSchema, UpdateUserSchema } from "@dbschema/edgedb-zod/modules/users"; +import { + CreateInfractionSchema as CreateInfractionSchema_, + CreateUserSchema, + UpdateUserSchema, +} from "@dbschema/edgedb-zod/modules/users"; import { createZodDto } from "nestjs-zod"; import { z } from "zod"; export class CreateUserDto extends createZodDto(CreateUserSchema) {} export class UpdateUserDto extends createZodDto(UpdateUserSchema) {} + +const CreateInfractionSchema = CreateInfractionSchema_.omit({ duration: true }).extend({ + duration: z.number().optional(), +}); export class CreateInfractionDto extends createZodDto(CreateInfractionSchema) {} export const AddInPersonTrainingSchema = z.object({ rep_id: z.string(), - created_at: z.date(), + created_at: z.string().datetime(), }); export class AddInPersonTrainingDto extends createZodDto(AddInPersonTrainingSchema) {} diff --git a/apps/anvil/src/users/users.service.ts b/apps/anvil/src/users/users.service.ts index 54a1881..32c8a07 100644 --- a/apps/anvil/src/users/users.service.ts +++ b/apps/anvil/src/users/users.service.ts @@ -3,11 +3,19 @@ import { LdapUser } from "@/auth/interfaces/ldap-user.interface"; import { EdgeDBService } from "@/edgedb/edgedb.service"; import { LdapService } from "@/ldap/ldap.service"; import e from "@dbschema/edgeql-js"; +import { addInPersonTraining } from "@dbschema/queries/addInPersonTraining.query"; import { users } from "@ignis/types"; import { Location } from "@ignis/types/sign_in"; import { RepStatus, SignInStat, Training, User } from "@ignis/types/users"; import { ConflictException, Injectable, NotFoundException } from "@nestjs/common"; -import { CardinalityViolationError, ConstraintViolationError, Duration, InvalidValueError } from "edgedb"; +import { + CardinalityViolationError, + ConstraintViolationError, + Duration, + InvalidValueError, + RelativeDuration, +} from "edgedb"; +import { parseHumanDurationString } from "edgedb/dist/datatypes/datetime"; import { AddInPersonTrainingDto, CreateInfractionDto, @@ -350,68 +358,58 @@ export class UsersService { async addInPersonTraining(id: string, training_id: string, data: AddInPersonTrainingDto) { // pre-condition they must already have completed online training otherwise this is a no-op - await this.dbService.query( - e.assert_exists( - e.update(e.users.User, (u) => ({ - filter_single: { id }, - set: { - training: e.select(u.training, (t) => ({ - filter_single: e.op(t.id, "=", training_id), - "@created_at": t["@created_at"], - "@in_person_created_at": data.created_at, - "@in_person_signed_off_by": data.rep_id, - })), - }, - })), - ), - ); + await addInPersonTraining(this.dbService.client, { + id, + training_id, + ...data, + created_at: new Date(data.created_at), + }); } async revokeTraining(id: string, training_id: string, data: RevokeTrainingDto) { + const user = e.assert_exists(e.select(e.users.User, () => ({ filter_single: { id } }))); + await this.dbService.query( + e.update(user, () => ({ + set: { + training: { + "-=": e.assert_exists(e.select(e.users.User, UserTrainingEntry(id, training_id))).training, + }, + infractions: { + "+=": e.insert(e.users.Infraction, { + user, + reason: data.reason, + resolved: true, + type: e.users.InfractionType.TRAINING_ISSUE, + }), + }, + }, + })), + ); await this.dbService.query( e.delete(e.training.UserTrainingSession, (session) => ({ filter_single: e.all( - e.set( - e.op(session.training.id, "=", e.cast(e.uuid, training_id)), - e.op( - session.user, - "=", - e.update(e.users.User, (user) => ({ - set: { - training: { - "-=": e.assert_exists(e.select(e.users.User, UserTrainingEntry(id, training_id))).training, - }, - infractions: { - "+=": e.insert(e.users.Infraction, { - user, - reason: data.reason, - resolved: true, - type: e.users.InfractionType.TRAINING_ISSUE, - }), - }, - }, - filter_single: { id }, - })), - ), - ), + e.set(e.op(session.training.id, "=", e.cast(e.uuid, training_id)), e.op(session.user, "=", user)), ), })), ); } async addInfraction(id: string, data: CreateInfractionDto) { - await this.dbService.query( - e.update(e.users.User, (user) => ({ + const user = e.assert_exists(e.select(e.users.User, () => ({ filter_single: { id } }))); + return await this.dbService.query( + e.update(user, () => ({ set: { infractions: { "+=": e.insert(e.users.Infraction, { user, - ...data, - duration: Duration.from(data.duration!), + created_at: data.created_at, + reason: data.reason, + resolved: data.resolved, + type: data.type, + duration: data.duration ? new Duration(0, 0, 0, 0, data.duration) : undefined, }), }, }, - filter_single: { id }, })), ); } diff --git a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard.tsx b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard.tsx index b3c5f86..2085b7d 100644 --- a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard.tsx +++ b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard.tsx @@ -25,7 +25,7 @@ import { Separator } from "@ui/components/ui/separator"; import { Textarea } from "@ui/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@ui/components/ui/tooltip"; import { cn } from "@ui/lib/utils"; -import { addDays, format } from "date-fns"; +import { addDays, addMonths, format, startOfDay } from "date-fns"; import { CalendarIcon, LogOut, Plus } from "lucide-react"; import * as React from "react"; import { DateRange } from "react-day-picker"; @@ -116,7 +116,14 @@ const TrainingSection: React.FC = ({ user, location, onShiftReps