diff --git a/actions/committee.actions.ts b/actions/committee.actions.ts new file mode 100644 index 00000000..0867d05c --- /dev/null +++ b/actions/committee.actions.ts @@ -0,0 +1,52 @@ +'use server'; + +import { and, count, eq } from 'drizzle-orm'; + +import { committeeMeetings, db } from '~/server/db'; + +export const addEditMeeting = async ( + place: string, + agendaUrl: string, + minutesUrl: string, + createdAt: Date, + type: (typeof committeeMeetings.committeeType.enumValues)[number], + number?: number +): Promise<{ status: 'success' | 'error' }> => { + try { + if (number) { + await db + .update(committeeMeetings) + .set({ place, agendaUrl, minutesUrl, createdAt }) + .where( + and( + eq(committeeMeetings.meetingNumber, number), + eq(committeeMeetings.committeeType, type) + ) + ); + } else { + await db.transaction(async (trx) => { + const [{ count: meetingCount }] = await trx + .select({ count: count() }) + .from(committeeMeetings) + .where(eq(committeeMeetings.committeeType, type)); + + await trx.insert(committeeMeetings).values({ + place, + agendaUrl, + minutesUrl, + createdAt, + committeeType: type, + meetingNumber: meetingCount + 1, + }); + }); + } + return { + status: 'success', + }; + } catch (error) { + console.error(error); + return { + status: 'error', + }; + } +}; diff --git a/app/[locale]/@modals/(.)edit/client-utils.tsx b/app/[locale]/@modals/(.)edit/client-utils.tsx new file mode 100644 index 00000000..307f6fe4 --- /dev/null +++ b/app/[locale]/@modals/(.)edit/client-utils.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useRouter } from 'next/navigation'; +import { z } from 'zod'; + +import { Button } from '~/components/buttons'; +import { Input } from '~/components/inputs'; +import { FormField, FormItem, FormMessage } from '~/components/ui'; +import type { committeeMeetings } from '~/server/db'; +import { type Translations } from '~/i18n/translations'; +import { addEditMeeting } from '~/actions/committee.actions'; +import { useToast } from '~/lib/hooks'; + +export default function MeetingEdit({ + number, + meetingType, + existingData, + text, +}: { + number?: number; + meetingType: (typeof committeeMeetings.committeeType.enumValues)[number]; + existingData?: Omit< + typeof committeeMeetings.$inferSelect, + 'meetingNumber' | 'committeeType' + >; + text: Translations['Committee']; +}) { + const { toast } = useToast(); + const router = useRouter(); + const meetingSchema = z + .object({ + place: z.string().min(1, { + message: `${text.meetings.place} ${text.errors.required}`, + }), + agendaUrl: z.string().url({ + message: `${text.errors.invalid} ${text.meetings.agenda} ${text.edit.url}`, + }), + minutesUrl: z.string().url({ + message: `${text.errors.invalid} ${text.meetings.minutes} ${text.edit.url}`, + }), + createdDate: z + .string() + .min(1, { message: `${text.meetings.date} ${text.errors.required}` }) + .refine( + (value) => { + const date = new Date(value); + return !isNaN(date.getTime()); + }, + { + message: `${text.errors.invalid} ${text.meetings.date}`, + } + ), + createdTime: z + .string() + .regex(new RegExp(/^([0-1][0-9]|2[0-3]):[0-5][0-9]/), { + message: `${text.errors.invalid} ${text.edit.time}`, + }), + }) + .transform((data) => { + const { createdDate, createdTime, ...rest } = data; + const [hours, minutes] = createdTime.split(':').map(Number); + const createdAt = new Date(createdDate); + createdAt.setHours(hours, minutes); + + return { + ...rest, + createdAt, + }; + }); + type meetingOutput = z.infer; + type meetingInput = Omit & { + createdDate: string; + createdTime: string; + }; + const form = useForm({ + resolver: zodResolver(meetingSchema), + defaultValues: { + place: existingData?.place, + agendaUrl: existingData?.agendaUrl, + minutesUrl: existingData?.minutesUrl, + createdDate: + existingData && + `${existingData.createdAt.getFullYear()}-${(existingData.createdAt.getMonth() + 1).toString().padStart(2, '0')}-${existingData.createdAt.getDate().toString().padStart(2, '0')}`, + createdTime: `${existingData?.createdAt.getHours().toString().padStart(2, '0')}:${existingData?.createdAt.getMinutes().toString().padStart(2, '0')}`, + }, + }); + + const addOrEditMeeting = async (data: meetingOutput) => { + const result = await addEditMeeting( + data.place, + data.agendaUrl, + data.minutesUrl, + data.createdAt, + meetingType, + number + ); + toast({ + title: result.status === 'success' ? text.edit.success : text.edit.error, + variant: result.status, + }); + router.back(); + }; + + return ( + <> +

+ {number ? text.edit.edit : text.edit.add} + {` ${meetingType.charAt(0).toUpperCase() + meetingType.slice(1)} ${number ? text.meetings.serial + number : text.edit.meeting}`} +

+ +
+ ( + + + + + )} + /> + + ( + + + + + )} + /> + ( + + + + + )} + /> + + ( + + + + + )} + /> + ( + + + + + )} + /> + + + + + +
+ + ); +} diff --git a/app/[locale]/@modals/(.)edit/page.tsx b/app/[locale]/@modals/(.)edit/page.tsx new file mode 100644 index 00000000..c565e160 --- /dev/null +++ b/app/[locale]/@modals/(.)edit/page.tsx @@ -0,0 +1,92 @@ +import { Suspense } from 'react'; + +import { Dialog } from '~/components/dialog'; +import { cn } from '~/lib/utils'; +import { type committeeMeetings, db } from '~/server/db'; +import { getTranslations } from '~/i18n/translations'; +import NotFound from '~/app/[...catchAll]/page'; +import Loading from '~/components/loading'; + +import MeetingEdit from './client-utils'; + +export default async function Edit({ + params: { locale }, + searchParams: { no: number, type: categoryToEdit }, +}: { + params: { locale: string }; + searchParams: { + no?: string; + type: (typeof committeeMeetings.committeeType.enumValues)[number]; + }; +}) { + const text = (await getTranslations(locale)).Committee; + const renderContent = async () => { + if ( + categoryToEdit === undefined || + !['building', 'financial', 'governor', 'senate'].includes(categoryToEdit) + ) + return ( + + ); + + const existingData = !isNaN(Number(number)) + ? await db.query.committeeMeetings.findFirst({ + where: (meeting, { eq, and }) => + and( + eq(meeting.committeeType, categoryToEdit), + eq(meeting.meetingNumber, Number(number)) + ), + columns: { + id: true, + place: true, + agendaUrl: true, + createdAt: true, + minutesUrl: true, + }, + }) + : undefined; + if (!!!existingData && !isNaN(Number(number))) { + return ( + + ); + } + return ( + + ); + }; + return ( + +
+ }> + {renderContent()} + +
+
+ ); +} diff --git a/app/[locale]/institute/administration/(committees)/committee.tsx b/app/[locale]/institute/administration/(committees)/committee.tsx index 62f846a4..8a7b4760 100644 --- a/app/[locale]/institute/administration/(committees)/committee.tsx +++ b/app/[locale]/institute/administration/(committees)/committee.tsx @@ -29,6 +29,7 @@ export default async function Committee({ type: (typeof committeeMembers.committeeType.enumValues)[number]; }) { const text = (await getTranslations(locale)).Committee; + const isAdmin = true; //isAdmin(); const meetingPage = isNaN(Number(searchParams.meetingPage ?? '1')) ? 1 @@ -74,10 +75,24 @@ export default async function Committee({ {text.meetings.minutes} + {isAdmin && ( + + + /{text.edit.edit} + + )} - + @@ -120,10 +135,12 @@ const Meetings = async ({ locale, page, type, + isAdmin, }: { locale: string; page: number; type: (typeof committeeMeetings.committeeType.enumValues)[number]; + isAdmin: boolean; }) => { const meetings = await db.query.committeeMeetings.findMany({ orderBy: (meeting, { desc }) => [desc(meeting.meetingNumber)], @@ -161,6 +178,18 @@ const Meetings = async ({ + {isAdmin && ( + + + + )} )); }; diff --git a/components/ui/form.tsx b/components/ui/form.tsx index 24ef9152..50f7caeb 100644 --- a/components/ui/form.tsx +++ b/components/ui/form.tsx @@ -58,6 +58,7 @@ const useFormField = () => { interface FormItemContextValue { id: string; + reserveSpaceForError?: boolean; } const FormItemContext = React.createContext( @@ -66,13 +67,21 @@ const FormItemContext = React.createContext( const FormItem = React.forwardRef< HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { + React.HTMLAttributes & { reserveSpaceForError?: boolean } +>(({ className, reserveSpaceForError, ...props }, ref) => { const id = React.useId(); return ( - -
+ +
); }); @@ -143,6 +152,7 @@ const FormMessage = React.forwardRef< React.HTMLAttributes >(({ className, children, ...props }, ref) => { const { error, formMessageId } = useFormField(); + const { reserveSpaceForError } = React.useContext(FormItemContext); const body = error ? String(error?.message) : children; if (!body) { @@ -153,7 +163,11 @@ const FormMessage = React.forwardRef<

{body} diff --git a/components/ui/index.ts b/components/ui/index.ts index df227aaa..9e3cf515 100644 --- a/components/ui/index.ts +++ b/components/ui/index.ts @@ -3,6 +3,7 @@ export * from './badge'; export * from './card'; export * from './command'; export * from './dialog'; +export * from './form'; export * from './label'; export * from './radio-group'; export * from './scroll-area'; diff --git a/i18n/en.ts b/i18n/en.ts index 481e525d..f673366b 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -41,6 +41,21 @@ const text: Translations = { agenda: 'Agenda', minutes: 'Minutes', }, + errors: { + required: 'is required', + invalid: 'invalid', + }, + edit: { + url: 'url', + time: 'time', + add: 'Add', + edit: 'Edit', + submit: 'submit', + meeting: 'Meeting', + success: 'The meeting has been successfully added/edited', + error: 'An error occurred. Please try again later', + back: 'back', + }, }, Curricula: { pageTitle: 'CURRICULA', diff --git a/i18n/hi.ts b/i18n/hi.ts index 8af092bd..e99b1410 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -37,6 +37,21 @@ const text: Translations = { agenda: 'कार्यसूची', minutes: 'विवरण', }, + errors: { + required: 'आवश्यक है', + invalid: 'अमान्य', + }, + edit: { + url: 'यूआरएल', + time: 'time', + add: 'जोड़ें', + edit: 'संपादित करें', + submit: 'प्रस्तुत', + success: 'बैठक सफलतापूर्वक जोड़ी गई/संपादित की गई है', + error: 'कोई त्रुटि हुई है। कृपया बाद में पुनः प्रयास करें', + meeting: 'बैठक', + back: 'वापस', + }, }, Curricula: { pageTitle: 'पाठ्यक्रम', diff --git a/i18n/translations.ts b/i18n/translations.ts index 43127a89..be7ac114 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -33,6 +33,21 @@ export interface Translations { agenda: string; minutes: string; }; + errors: { + required: string; + invalid: string; + }; + edit: { + url: string; + time: string; + add: string; + edit: string; + meeting: string; + submit: string; + success: string; + error: string; + back: string; + }; }; Curricula: { pageTitle: string;