From 625d38d8df758f4308be4fb26d200e87384c66c8 Mon Sep 17 00:00:00 2001
From: Ethan Bickel
Date: Tue, 5 Nov 2024 17:11:45 -0600
Subject: [PATCH] add editing club listed officers and display them
---
.../edit/officers/EditListedOfficerForm.tsx | 217 ++++++++++++++++++
.../edit/officers/EditOfficerForm.tsx | 38 +--
.../manage/[clubId]/edit/officers/page.tsx | 9 +-
.../club/listing/ClubInfoSegment.tsx | 14 +-
src/server/api/routers/club.ts | 32 +--
src/server/api/routers/clubEdit.ts | 64 +++++-
src/server/db/index.ts | 2 +
src/utils/formSchemas.ts | 9 +
8 files changed, 316 insertions(+), 69 deletions(-)
create mode 100644 src/app/manage/[clubId]/edit/officers/EditListedOfficerForm.tsx
diff --git a/src/app/manage/[clubId]/edit/officers/EditListedOfficerForm.tsx b/src/app/manage/[clubId]/edit/officers/EditListedOfficerForm.tsx
new file mode 100644
index 00000000..3f8e99cd
--- /dev/null
+++ b/src/app/manage/[clubId]/edit/officers/EditListedOfficerForm.tsx
@@ -0,0 +1,217 @@
+/* eslint-disable @typescript-eslint/no-misused-promises */
+'use client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { api } from '@src/trpc/react';
+import { editListedOfficerSchema } from '@src/utils/formSchemas';
+import { useRouter } from 'next/navigation';
+import { useReducer } from 'react';
+import {
+ type FieldErrors,
+ type UseFormRegister,
+ useFieldArray,
+ useForm,
+} from 'react-hook-form';
+import { type z } from 'zod';
+
+type x = {
+ id?: boolean;
+ name?: boolean;
+ position?: boolean;
+}[];
+const modifiedFields = (
+ dirtyFields: x,
+ data: z.infer,
+ officers: {
+ id?: string;
+ name: string;
+ position: string;
+ }[],
+) => {
+ const modded = data.officers.filter(
+ (value, index) =>
+ !!officers.find((off) => off.id === value.id) &&
+ dirtyFields[index]?.position,
+ );
+ const created = data.officers.filter(
+ (value) => typeof value.id === 'undefined',
+ );
+ return {
+ modified: modded as { id: string; name: string; position: string }[],
+ created: created as { name: string; position: string }[],
+ };
+};
+
+type modifyDeletedAction =
+ | {
+ type: 'add';
+ target: string;
+ }
+ | { type: 'reset' };
+const deletedReducer = (state: Array, action: modifyDeletedAction) => {
+ switch (action.type) {
+ case 'add':
+ return [...state, action.target];
+ case 'reset':
+ return [];
+ }
+};
+
+type EditOfficerFormProps = {
+ clubId: string;
+ officers: {
+ id: string;
+ name: string;
+ position: string;
+ }[];
+};
+const EditOfficerForm = ({ clubId, officers }: EditOfficerFormProps) => {
+ const {
+ control,
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, dirtyFields, isDirty },
+ } = useForm>({
+ resolver: zodResolver(editListedOfficerSchema),
+ defaultValues: { officers: officers },
+ });
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: 'officers',
+ });
+ const [deleted, modifyDeleted] = useReducer(deletedReducer, []);
+ const removeItem = (index: number) => {
+ const off = officers.find((officer) => officer.id == fields[index]?.id);
+ if (off) modifyDeleted({ type: 'add', target: off.id });
+ remove(index);
+ };
+ const router = useRouter();
+ const editOfficers = api.club.edit.listedOfficers.useMutation({
+ onSuccess: () => {
+ router.push(`/directory/${clubId}`);
+ },
+ });
+ const submitForm = handleSubmit((data) => {
+ if (dirtyFields.officers !== undefined) {
+ const { modified, created } = modifiedFields(
+ dirtyFields.officers,
+ data,
+ officers,
+ );
+ if (!editOfficers.isPending) {
+ editOfficers.mutate({
+ clubId: clubId,
+ deleted: deleted,
+ modified: modified,
+ created: created,
+ });
+ }
+ }
+ });
+ return (
+
+ );
+};
+export default EditOfficerForm;
+type OfficerItemProps = {
+ register: UseFormRegister>;
+ remove: (index: number) => void;
+ index: number;
+ errors: FieldErrors>;
+};
+const OfficerItem = ({ register, index, remove, errors }: OfficerItemProps) => {
+ return (
+
+
+
+
+ {errors.officers && errors.officers[index]?.position && (
+
+ {errors.officers[index]?.position?.message}
+
+ )}
+
+
+
+ {errors.officers && errors.officers[index]?.position && (
+
+ {errors.officers[index]?.position?.message}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/app/manage/[clubId]/edit/officers/EditOfficerForm.tsx b/src/app/manage/[clubId]/edit/officers/EditOfficerForm.tsx
index f0f027d7..aacdb39b 100644
--- a/src/app/manage/[clubId]/edit/officers/EditOfficerForm.tsx
+++ b/src/app/manage/[clubId]/edit/officers/EditOfficerForm.tsx
@@ -6,12 +6,7 @@ import { api } from '@src/trpc/react';
import { editOfficerSchema } from '@src/utils/formSchemas';
import { useRouter } from 'next/navigation';
import { useReducer } from 'react';
-import {
- type FieldErrors,
- type UseFormRegister,
- useFieldArray,
- useForm,
-} from 'react-hook-form';
+import { useFieldArray, useForm } from 'react-hook-form';
import { type z } from 'zod';
type x = {
@@ -69,14 +64,12 @@ type EditOfficerFormProps = {
userId: string;
name: string;
locked: boolean;
- title: string;
position: 'President' | 'Officer';
}[];
};
const EditOfficerForm = ({ clubId, officers }: EditOfficerFormProps) => {
const {
control,
- register,
handleSubmit,
reset,
formState: { errors, dirtyFields, isDirty },
@@ -143,11 +136,9 @@ const EditOfficerForm = ({ clubId, officers }: EditOfficerFormProps) => {
{fields.map((field, index) => (
@@ -181,23 +172,13 @@ const EditOfficerForm = ({ clubId, officers }: EditOfficerFormProps) => {
};
export default EditOfficerForm;
type OfficerItemProps = {
- register: UseFormRegister>;
remove: (index: number, userId: string) => void;
id: string;
index: number;
name: string;
locked: boolean;
- errors: FieldErrors>;
};
-const OfficerItem = ({
- register,
- index,
- id,
- name,
- remove,
- errors,
- locked,
-}: OfficerItemProps) => {
+const OfficerItem = ({ index, id, name, remove, locked }: OfficerItemProps) => {
return (
@@ -206,21 +187,6 @@ const OfficerItem = ({
{name}
-
-
- {errors.officers && errors.officers[index]?.title && (
-
- {errors.officers[index]?.title?.message}
-
- )}
-
);
diff --git a/src/components/club/listing/ClubInfoSegment.tsx b/src/components/club/listing/ClubInfoSegment.tsx
index 1654ea64..ebd2a307 100644
--- a/src/components/club/listing/ClubInfoSegment.tsx
+++ b/src/components/club/listing/ClubInfoSegment.tsx
@@ -7,7 +7,7 @@ const ClubInfoSegment: FC<{
club: NonNullable;
}> = async ({ club }) => {
const isActive = await api.club.isActive({ id: club.id });
- const president = club.userMetadataToClubs.find(
+ const president = (await api.club.getOfficers({ id: club.id })).find(
(officer) => officer.memberType === 'President',
);
return (
@@ -52,13 +52,13 @@ const ClubInfoSegment: FC<{
{club.description}
- {club.userMetadataToClubs.length != 0 && (
+ {club.officers.length != 0 && (
<>
Leadership
- {club.userMetadataToClubs.map((officer) => (
-
+ {club.officers.map((officer) => (
+
- {officer.userMetadata.firstName +
- ' ' +
- officer.userMetadata.lastName}
+ {officer.name}
- Officer {/* TODO: link to officers table */}
+ {officer.position}
diff --git a/src/server/api/routers/club.ts b/src/server/api/routers/club.ts
index 07cd3887..36f6dafc 100644
--- a/src/server/api/routers/club.ts
+++ b/src/server/api/routers/club.ts
@@ -1,14 +1,4 @@
-import {
- eq,
- ilike,
- sql,
- and,
- notInArray,
- inArray,
- or,
- lt,
- gt,
-} from 'drizzle-orm';
+import { eq, ilike, sql, and, notInArray, inArray, lt, gt } from 'drizzle-orm';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { z } from 'zod';
import { clubEditRouter } from './clubEdit';
@@ -16,6 +6,7 @@ import { userMetadataToClubs } from '@src/server/db/schema/users';
import { club, usedTags } from '@src/server/db/schema/club';
import { contacts } from '@src/server/db/schema/contacts';
import { carousel } from '@src/server/db/schema/admin';
+import { officers as officersTable } from '@src/server/db/schema/officers';
import { createClubSchema as baseClubSchema } from '@src/utils/formSchemas';
const byNameSchema = z.object({
name: z.string().default(''),
@@ -252,6 +243,14 @@ export const clubRouter = createTRPCRouter({
});
return officers;
}),
+ getListedOfficers: protectedProcedure
+ .input(byIdSchema)
+ .query(async ({ input, ctx }) => {
+ const officers = await ctx.db.query.officers.findMany({
+ where: eq(officersTable.clubId, input.id),
+ });
+ return officers;
+ }),
isActive: publicProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
const hasPresident = await ctx.db.query.userMetadataToClubs.findFirst({
where: and(
@@ -278,16 +277,7 @@ export const clubRouter = createTRPCRouter({
where: (club) => eq(club.id, id),
with: {
contacts: true,
- userMetadataToClubs: {
- where: (row) =>
- or(
- eq(row.memberType, 'President'),
- eq(row.memberType, 'Officer'),
- ),
- with: {
- userMetadata: { columns: { firstName: true, lastName: true } },
- },
- },
+ officers: true,
},
});
return byId;
diff --git a/src/server/api/routers/clubEdit.ts b/src/server/api/routers/clubEdit.ts
index 30ca549c..3dfbe4de 100644
--- a/src/server/api/routers/clubEdit.ts
+++ b/src/server/api/routers/clubEdit.ts
@@ -8,6 +8,7 @@ import { selectContact } from '@src/server/db/models';
import { club } from '@src/server/db/schema/club';
import { contacts } from '@src/server/db/schema/contacts';
import { userMetadataToClubs } from '@src/server/db/schema/users';
+import { officers } from '@src/server/db/schema/officers';
async function isUserOfficer(userId: string, clubId: string) {
const officer = await db.query.userMetadataToClubs.findFirst({
@@ -36,7 +37,7 @@ const editContactSchema = z.object({
modified: selectContact.array(),
created: selectContact.omit({ clubId: true }).array(),
});
-const editOfficerSchema = z.object({
+const editCollaboratorSchema = z.object({
clubId: z.string(),
deleted: z.string().array(),
modified: z
@@ -53,6 +54,24 @@ const editOfficerSchema = z.object({
})
.array(),
});
+
+const editOfficerSchema = z.object({
+ clubId: z.string(),
+ deleted: z.string().array(),
+ modified: z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ position: z.string(),
+ })
+ .array(),
+ created: z
+ .object({
+ name: z.string(),
+ position: z.string(),
+ })
+ .array(),
+});
const deleteSchema = z.object({ clubId: z.string() });
export const clubEditRouter = createTRPCRouter({
@@ -117,7 +136,7 @@ export const clubEditRouter = createTRPCRouter({
.onConflictDoNothing();
}),
officers: protectedProcedure
- .input(editOfficerSchema)
+ .input(editCollaboratorSchema)
.mutation(async ({ input, ctx }) => {
const isOfficer = await isUserOfficer(ctx.session.user.id, input.clubId);
if (!isOfficer) {
@@ -169,6 +188,47 @@ export const clubEditRouter = createTRPCRouter({
where: eq(userMetadataToClubs.memberType, 'Member'),
});
}),
+ listedOfficers: protectedProcedure
+ .input(editOfficerSchema)
+ .mutation(async ({ input, ctx }) => {
+ const isOfficer = await isUserOfficer(ctx.session.user.id, input.clubId);
+ if (!isOfficer) {
+ throw new TRPCError({
+ message: 'must be an officer to modify this club',
+ code: 'UNAUTHORIZED',
+ });
+ }
+ if (input.deleted.length > 0) {
+ await ctx.db
+ .delete(userMetadataToClubs)
+ .where(
+ and(
+ eq(userMetadataToClubs.clubId, input.clubId),
+ inArray(userMetadataToClubs.userId, input.deleted),
+ ),
+ );
+ }
+ const promises: Promise
[] = [];
+ for (const modded of input.modified) {
+ const prom = ctx.db
+ .update(officers)
+ .set({ position: modded.position })
+ .where(
+ and(eq(officers.id, modded.id), eq(officers.clubId, input.clubId)),
+ );
+ promises.push(prom);
+ }
+ await Promise.allSettled(promises);
+ if (input.created.length === 0) return;
+
+ await ctx.db.insert(officers).values(
+ input.created.map((officer) => ({
+ clubId: input.clubId,
+ name: officer.name,
+ position: officer.position,
+ })),
+ );
+ }),
delete: protectedProcedure
.input(deleteSchema)
.mutation(async ({ input, ctx }) => {
diff --git a/src/server/db/index.ts b/src/server/db/index.ts
index cc1185ab..59295784 100644
--- a/src/server/db/index.ts
+++ b/src/server/db/index.ts
@@ -7,6 +7,7 @@ import * as events from './schema/events';
import * as users from './schema/users';
import * as forms from './schema/forms';
import * as admin from './schema/admin';
+import * as officers from './schema/officers';
import { neon } from '@neondatabase/serverless';
const schema = {
@@ -16,6 +17,7 @@ const schema = {
...users,
...forms,
...admin,
+ ...officers,
};
const neon_client = neon(env.DATABASE_URL);
diff --git a/src/utils/formSchemas.ts b/src/utils/formSchemas.ts
index 2cc635bb..77dc0bf7 100644
--- a/src/utils/formSchemas.ts
+++ b/src/utils/formSchemas.ts
@@ -36,6 +36,15 @@ export const editOfficerSchema = z.object({
})
.array(),
});
+export const editListedOfficerSchema = z.object({
+ officers: z
+ .object({
+ id: z.string().optional(),
+ name: z.string(),
+ position: z.string().min(1),
+ })
+ .array(),
+});
export const createEventSchema = z.object({
clubId: z.string(),