import { useEffect, useMemo, useState } from 'react'; import { Button, Text, Alert, Tabs, FileInput, Loader, Flex, useMantineTheme } from '@mantine/core'; import { ActionArgs, json, LoaderArgs, MetaFunction, redirect } from '@remix-run/node'; import { Form, Link, useActionData, useSearchParams } from '@remix-run/react'; import { CheckCircle, Download, Upload, XCircle } from 'react-feather'; import { requireUserId } from '~/session.server'; import { createProject, getProjectByName } from '~/models/project.server'; import { createTimeEntry } from '~/models/timeEntry.server'; import papaparse from 'papaparse'; import { randomColorName } from '~/utils'; export const meta: MetaFunction = () => { return { title: 'Import/Export | WorkTimer', description: 'Manage your projects. You must be logged in to do this.' }; }; export async function action({ request, params }: ActionArgs) { const userId = await requireUserId(request); const formData = await request.formData(); const fileData = formData.get('fileData'); if (typeof fileData !== 'string' || !fileData?.length) { return json( { errors: { fileData: 'No file data' }, success: false }, { status: 400 } ); } const parsed = papaparse.parse(fileData, { header: true, skipEmptyLines: true, dynamicTyping: true }); if (parsed.errors.length) { return json( { errors: { fileData: parsed.errors[0].message }, success: false, imported: 0 }, { status: 400 } ); } const headers = parsed.meta.fields; if (!headers?.includes('description')) { return json( { errors: { fileData: 'Missing description column' }, success: false, imported: 0 }, { status: 400 } ); } if (!headers?.includes('startTime')) { return json( { errors: { fileData: 'Missing startTime column' }, success: false, imported: 0 }, { status: 400 } ); } if (!headers?.includes('endTime')) { return json( { errors: { fileData: 'Missing endTime column' }, success: false, imported: 0 }, { status: 400 } ); } if (!headers?.includes('project')) { return json( { errors: { fileData: 'Missing project column' }, success: false, imported: 0 }, { status: 400 } ); } const timeEntries = parsed.data.map<{ description: string; startTime: string; endTime?: string; duration?: number; projectId?: string; projectName: string; }>((row: any) => ({ description: row.description, startTime: row.startTime, endTime: row.endTime, duration: row.duration, projectId: undefined, projectName: row.project })); for (const timeEntry of timeEntries) { const project = await getProjectByName({ userId, name: timeEntry.projectName }); if (!project) { const project = await createProject({ userId, name: timeEntry.projectName, description: null, color: randomColorName() }); timeEntry.projectId = project.id; } else { timeEntry.projectId = project.id; } await createTimeEntry({ userId, projectId: timeEntry.projectId, description: timeEntry.description, startTime: new Date(timeEntry.startTime), endTime: timeEntry.endTime ? new Date(timeEntry.endTime) : null, duration: timeEntry.duration ? timeEntry.duration : timeEntry.endTime && timeEntry.startTime ? new Date(timeEntry.endTime).getTime() - new Date(timeEntry.startTime).getTime() : null }); } return json( { errors: { fileData: null }, success: true, imported: timeEntries.length }, { status: 200 } ); } export async function loader({ request }: LoaderArgs) { const userId = await requireUserId(request); if (!userId) return redirect('/login'); return json({}); } export default function ImportExportPage() { const actionData = useActionData(); const [searchParams, setSearchParams] = useSearchParams(); const tab = useMemo(() => { return searchParams.get('tab') || 'import'; }, [searchParams]); useEffect(() => { setSearchParams({ tab }); }, []); const [csvData, setCsvData] = useState(); useEffect(() => { if (actionData?.success) { setTimeout(() => { window.location.pathname = '/time-entries'; }, 3000); } }, [actionData?.success]); const handleChangeFile = (file: File) => { let reader: FileReader = new FileReader(); reader.onload = (_event: Event) => { setCsvData(reader.result as string); }; reader.readAsText(file, 'UTF-8'); }; return (

Import/Export

{ setSearchParams({ tab: tab as string }); }} > }> Import }> Export

Import

Select a CSV file with the same format as the one you can download from Export

} onChange={handleChangeFile} aria-invalid={actionData?.errors?.fileData ? true : undefined} error={actionData?.errors?.fileData} errorProps={{ children: actionData?.errors?.fileData }} />
{!!actionData?.success && ( } title="Import successful" color="green" radius="md" variant="light" mt="md" withCloseButton closeButtonLabel="Close results" > Successfully imported time entries Redirecting to time entries... )} {!!actionData?.errors?.fileData && ( } title="Error" color="red" radius="md" variant="light" mt="md" withCloseButton closeButtonLabel="Close results" > {actionData?.errors?.fileData} )}

Export

Export all your time entries as CSV file

); }