diff --git a/app/models/timeEntry.server.ts b/app/models/timeEntry.server.ts index 7752a87..98d1d8e 100644 --- a/app/models/timeEntry.server.ts +++ b/app/models/timeEntry.server.ts @@ -161,3 +161,28 @@ export function deleteTimeEntry({ where: { id, userId } }); } + +export async function exportTimeEntries({ userId }: { userId: User['id'] }) { + const entries = await prisma.timeEntry.findMany({ + where: { + userId + }, + select: { + id: true, + description: true, + startTime: true, + endTime: true, + createdAt: true, + project: { + select: { + name: true + } + } + } + }); + + return entries.map((entry) => ({ + ...entry, + project: entry.project?.name + })); +} diff --git a/app/routes/importexport.tsx b/app/routes/importexport.tsx new file mode 100644 index 0000000..774f715 --- /dev/null +++ b/app/routes/importexport.tsx @@ -0,0 +1,161 @@ +import { useEffect, useMemo } from 'react'; +import { + Button, + Paper, + Text, + Menu, + ActionIcon, + Pagination, + NativeSelect, + Group, + useMantineTheme, + Alert, + ColorSwatch, + Tabs +} from '@mantine/core'; +import { + ActionArgs, + json, + LoaderArgs, + MetaFunction, + redirect +} from '@remix-run/node'; +import { + Form, + Link, + Outlet, + useActionData, + useCatch, + useLoaderData, + useSearchParams +} from '@remix-run/react'; +import { + AlertTriangle, + Download, + Edit3, + Plus, + Settings, + Trash, + Upload +} from 'react-feather'; +import { requireUserId } from '~/session.server'; +import { getProjects } from '~/models/project.server'; +import { exportTimeEntries, getTimeEntries } from '~/models/timeEntry.server'; +import papaparse from 'papaparse'; + +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 actionType = formData.get('type'); + + if ( + typeof actionType !== 'string' || + !['import', 'export'].includes(actionType) + ) { + return json( + { + errors: { + type: 'Invalid action type', + data: null + } + }, + { + status: 400 + } + ); + } + + if (actionType === 'import') { + const file = formData.get('file'); + + return json({}, { status: 200 }); + } else if (actionType === 'export') { + const timeEntries = await exportTimeEntries({ userId }); + const csv = papaparse.unparse(timeEntries, { + header: true + }); + + return json( + { + headers: { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename="export.csv"' + }, + body: 'hello world', + errors: {} + }, + { + 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 data = useLoaderData(); + const theme = useMantineTheme(); + const [searchParams, setSearchParams] = useSearchParams(); + + const tab = useMemo(() => { + return searchParams.get('tab') || 'import'; + }, [searchParams]); + + useEffect(() => { + setSearchParams({ tab }); + }, []); + + return ( +
+ { + setSearchParams({ tab: tab as string }); + }} + > + + }> + Import + + }> + Export + + + + +

Import CSV

+
+ + +

Export

+

Export all your time entries as CSV file

+ +
+
+
+ ); +} diff --git a/app/routes/importexport/export[.]csv.tsx b/app/routes/importexport/export[.]csv.tsx new file mode 100644 index 0000000..e9f4d49 --- /dev/null +++ b/app/routes/importexport/export[.]csv.tsx @@ -0,0 +1,21 @@ +import { json, LoaderArgs } from '@remix-run/node'; +import { requireUserId } from '~/session.server'; +import { exportTimeEntries } from '~/models/timeEntry.server'; +import papaparse from 'papaparse'; + +export async function loader({ request }: LoaderArgs) { + const userId = await requireUserId(request); + + const timeEntries = await exportTimeEntries({ userId }); + const csv = papaparse.unparse(timeEntries, { + header: true + }); + + return new Response(csv, { + status: 200, + headers: { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename="export.csv"' + } + }); +} diff --git a/package.json b/package.json index 438ad47..80dea8a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dayjs": "1.11.7", "isbot": "^3.6.5", "nprogress": "0.2.0", + "papaparse": "5.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-feather": "2.0.10", @@ -43,6 +44,7 @@ "@types/bcryptjs": "^2.4.2", "@types/node": "^18.11.18", "@types/nprogress": "0.2.0", + "@types/papaparse": "5.3.7", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.8", "eslint": "^8.27.0", diff --git a/yarn.lock b/yarn.lock index ecaf9c9..3d5479b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2390,6 +2390,13 @@ resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f" integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A== +"@types/papaparse@5.3.7": + version "5.3.7" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.7.tgz#8d3bf9e62ac2897df596f49d9ca59a15451aa247" + integrity sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg== + dependencies: + "@types/node" "*" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -7528,6 +7535,11 @@ pako@~0.2.0: resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== +papaparse@5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467" + integrity sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"