From 9b16fe617cf62a525635c1fb0151b26035853766 Mon Sep 17 00:00:00 2001 From: nzambello Date: Tue, 14 Feb 2023 10:27:27 +0100 Subject: [PATCH] feat: add new + edit/delete time-entries --- app/routes/time-entries/$timeEntryId.tsx | 502 +++++++++++++++++++++-- app/routes/time-entries/index.tsx | 12 - app/routes/time-entries/new.tsx | 488 ++++++++++++++++++---- 3 files changed, 874 insertions(+), 128 deletions(-) delete mode 100644 app/routes/time-entries/index.tsx diff --git a/app/routes/time-entries/$timeEntryId.tsx b/app/routes/time-entries/$timeEntryId.tsx index e253a59..a040079 100644 --- a/app/routes/time-entries/$timeEntryId.tsx +++ b/app/routes/time-entries/$timeEntryId.tsx @@ -1,60 +1,496 @@ -import type { ActionArgs, LoaderArgs } from '@remix-run/node' -import { json, redirect } from '@remix-run/node' -import { Form, useCatch, useLoaderData } from '@remix-run/react' -import invariant from 'tiny-invariant' +import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node'; +import { json, redirect } from '@remix-run/node'; +import { + Form, + useActionData, + useCatch, + useLoaderData, + useNavigate +} from '@remix-run/react'; +import * as React from 'react'; +import { + Alert, + Drawer, + TextInput, + Text, + useMantineTheme, + Group, + Button, + Textarea, + Stack, + Select, + ColorSwatch, + ActionIcon +} from '@mantine/core'; +import { + AlertTriangle, + Delete, + Play, + Save, + Square, + Trash +} from 'react-feather'; +import invariant from 'tiny-invariant'; -import { deleteNote, getNote } from '~/models/note.server' -import { requireUserId } from '~/session.server' +import { + deleteTimeEntry, + getTimeEntry, + updateTimeEntry +} from '~/models/timeEntry.server'; +import { requireUserId } from '~/session.server'; +import { getProjects } from '~/models/project.server'; +import { DatePicker, TimeInput } from '@mantine/dates'; + +export const meta: MetaFunction = () => { + return { + title: 'Edit Time Entry | WorkTimer', + description: 'Edit a time entry. You must be logged in to do this.' + }; +}; export async function loader({ request, params }: LoaderArgs) { - const userId = await requireUserId(request) - invariant(params.noteId, 'noteId not found') + const userId = await requireUserId(request); + invariant(params.timeEntryId, 'timeEntryId not found'); - const note = await getNote({ userId, id: params.timeEntryId }) - if (!note) { - throw new Response('Not Found', { status: 404 }) + const timeEntry = await getTimeEntry({ userId, id: params.timeEntryId }); + if (!timeEntry) { + throw new Response('Not Found', { status: 404 }); } - return json({ note }) + + const projects = await getProjects({ userId }); + + return json({ timeEntry, projects }); } export async function action({ request, params }: ActionArgs) { - const userId = await requireUserId(request) - invariant(params.timeEntryId, 'timeEntryId not found') + const userId = await requireUserId(request); + invariant(params.timeEntryId, 'timeEntryId not found'); - await deleteNote({ userId, id: params.timeEntryId }) + const timeEntry = await getTimeEntry({ userId, id: params.timeEntryId }); + if (!timeEntry) { + throw new Response('Not Found', { status: 404 }); + } - return redirect('/notes') + if (request.method === 'DELETE') { + await deleteTimeEntry({ userId, id: params.timeEntryId }); + } else if (request.method === 'PATCH') { + const formData = await request.formData(); + + const description = (formData.get('description') || undefined) as + | string + | undefined; + const projectId = (formData.get('projectId') || undefined) as + | string + | undefined; + let startTime = (formData.get('startTime') || undefined) as + | string + | undefined; + let endTime = (formData.get('endTime') || undefined) as string | undefined; + + if ( + startTime && + typeof startTime === 'string' && + Number.isNaN(Date.parse(startTime)) + ) { + return json( + { + errors: { + projectId: null, + description: null, + startTime: 'startTime is invalid', + endTime: null + } + }, + { status: 422 } + ); + } + if ( + endTime && + typeof endTime === 'string' && + Number.isNaN(Date.parse(endTime)) + ) { + return json( + { + errors: { + projectId: null, + description: null, + startTime: null, + endTime: 'endTime is invalid' + } + }, + { status: 422 } + ); + } + if ( + startTime && + endTime && + typeof startTime === 'string' && + typeof endTime === 'string' && + new Date(startTime) > new Date(endTime) + ) { + return json( + { + errors: { + projectId: null, + description: null, + startTime: 'startTime must be before endTime', + endTime: 'startTime must be before endTime' + } + }, + { status: 422 } + ); + } + + await updateTimeEntry({ + timeEntryId: params.timeEntryId, + description, + projectId, + startTime: startTime ? new Date(startTime) : undefined, + endTime: endTime ? new Date(endTime) : undefined + }); + } + + return redirect('/time-entries'); } -export default function NoteDetailsPage() { - const data = useLoaderData() +interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { + id: string; + label: string; + color: string; +} - return ( -
-

{data.note.title}

-

{data.note.body}

-
-
- -
+const SelectItem = React.forwardRef( + ({ label, color, id, ...others }: ItemProps, ref) => ( +
+ + + {label} +
) +); + +const LayoutWrapper = ({ children }: React.PropsWithChildren<{}>) => { + const theme = useMantineTheme(); + const navigate = useNavigate(); + + return ( + { + navigate('/time-entries'); + }} + > + {children} + + ); +}; + +export default function TimeEntryDetailsPage() { + const actionData = useActionData(); + const data = useLoaderData(); + const theme = useMantineTheme(); + + const descriptionRef = React.useRef(null); + const startDateRef = React.useRef(null); + const endDateRef = React.useRef(null); + const projectRef = React.useRef(null); + + const [start, setStart] = React.useState( + new Date(data.timeEntry.startTime || Date.now()) + ); + const [end, setEnd] = React.useState( + data.timeEntry.endTime ? new Date(data.timeEntry.endTime) : undefined + ); + + React.useEffect(() => { + if (actionData?.errors?.description) { + descriptionRef.current?.focus(); + } else if (actionData?.errors?.startTime) { + startDateRef.current?.focus(); + } else if (actionData?.errors?.endTime) { + endDateRef.current?.focus(); + } else if (actionData?.errors?.projectId) { + projectRef.current?.focus(); + } + }, [actionData]); + + return ( + +
+