feat: add importexport page with csv export

This commit is contained in:
Nicola Zambello 2023-02-18 21:42:16 +01:00
parent 35adc7db03
commit 72df192ebc
Signed by: nzambello
GPG key ID: 56E4A92C2C1E50BA
5 changed files with 221 additions and 0 deletions

View file

@ -161,3 +161,28 @@ export function deleteTimeEntry({
where: { id, userId } 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
}));
}

161
app/routes/importexport.tsx Normal file
View file

@ -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<typeof action>();
const data = useLoaderData<typeof loader>();
const theme = useMantineTheme();
const [searchParams, setSearchParams] = useSearchParams();
const tab = useMemo(() => {
return searchParams.get('tab') || 'import';
}, [searchParams]);
useEffect(() => {
setSearchParams({ tab });
}, []);
return (
<div>
<Tabs
radius="sm"
defaultValue="import"
value={tab}
onTabChange={(tab) => {
setSearchParams({ tab: tab as string });
}}
>
<Tabs.List>
<Tabs.Tab value="import" icon={<Upload size={14} />}>
Import
</Tabs.Tab>
<Tabs.Tab value="export" icon={<Download size={14} />}>
Export
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="import" pt="xs">
<h2>Import CSV</h2>
</Tabs.Panel>
<Tabs.Panel value="export" pt="xs">
<h2>Export</h2>
<p>Export all your time entries as CSV file</p>
<Button
component="a"
href="/importexport/export.csv"
download="work-timer-export.csv"
leftIcon={<Download size={14} />}
>
Download CSV
</Button>
</Tabs.Panel>
</Tabs>
</div>
);
}

View file

@ -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"'
}
});
}

View file

@ -28,6 +28,7 @@
"dayjs": "1.11.7", "dayjs": "1.11.7",
"isbot": "^3.6.5", "isbot": "^3.6.5",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"papaparse": "5.3.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-feather": "2.0.10", "react-feather": "2.0.10",
@ -43,6 +44,7 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/papaparse": "5.3.7",
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8", "@types/react-dom": "^18.0.8",
"eslint": "^8.27.0", "eslint": "^8.27.0",

View file

@ -2390,6 +2390,13 @@
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f" resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A== 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": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" 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" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== 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: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"