diff --git a/app/routes/account.tsx b/app/routes/account.tsx
index a9b2c57..d0e49ef 100644
--- a/app/routes/account.tsx
+++ b/app/routes/account.tsx
@@ -21,7 +21,8 @@ import {
Progress,
Modal,
Badge,
- Select
+ Select,
+ NumberInput
} from '@mantine/core';
import { AtSign, Check, Lock, Save, Trash, X } from 'react-feather';
import { requireUser } from '~/session.server';
@@ -42,6 +43,12 @@ export async function action({ request }: ActionArgs) {
const dateFormat = (formData.get('dateFormat') || undefined) as
| string
| undefined;
+ const currency = (formData.get('currency') || undefined) as
+ | string
+ | undefined;
+ const defaultHourlyRate = (formData.get('defaultHourlyRate') || undefined) as
+ | string
+ | undefined;
if (email && !validateEmail(email)) {
return json(
@@ -59,8 +66,19 @@ export async function action({ request }: ActionArgs) {
await updateUserEmail(user.id, email);
}
- if (dateFormat && dateFormat !== user.dateFormat) {
- await updateUserPrefs(user.id, { dateFormat });
+ const prefs = {
+ dateFormat:
+ dateFormat && dateFormat !== user.dateFormat ? dateFormat : undefined,
+ currency: currency && currency !== user.currency ? currency : undefined,
+ defaultHourlyRate:
+ defaultHourlyRate &&
+ parseInt(defaultHourlyRate || '-1', 10) !== user.defaultHourlyRate
+ ? parseInt(defaultHourlyRate, 10)
+ : undefined
+ };
+
+ if (Object.values(prefs).some((v) => v !== undefined)) {
+ await updateUserPrefs(user.id, prefs);
}
return redirect('/account/updatesuccess');
@@ -90,6 +108,11 @@ export default function Account() {
loaderData.user.dateFormat
);
+ const [isHydrated, setIsHydrated] = React.useState(false);
+ React.useEffect(() => {
+ setIsHydrated(true);
+ }, []);
+
return (
@@ -188,23 +211,35 @@ export default function Account() {
])}
/>
- Example:
-
- {Intl.DateTimeFormat(dateFormat, {
- dateStyle: 'full',
- timeZone: 'UTC'
- }).format(new Date(Date.now()))}
-
- {Intl.DateTimeFormat(dateFormat, {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- }).format(new Date(Date.now()))}
-
+ {isHydrated && (
+
+ Example:{' '}
+ {Intl.DateTimeFormat(dateFormat, {
+ dateStyle: 'full',
+ timeStyle: 'short',
+ timeZone: 'UTC'
+ }).format(new Date(Date.now()))}
+
+ )}
-
+
+
+
+
+
}>
Save
diff --git a/app/routes/importexport.tsx b/app/routes/importexport.tsx
index 758e6e9..dd8f6de 100644
--- a/app/routes/importexport.tsx
+++ b/app/routes/importexport.tsx
@@ -46,7 +46,8 @@ export async function action({ request, params }: ActionArgs) {
const parsed = papaparse.parse(fileData, {
header: true,
- skipEmptyLines: true
+ skipEmptyLines: true,
+ dynamicTyping: true
});
if (parsed.errors.length) {
@@ -145,7 +146,7 @@ export async function action({ request, params }: ActionArgs) {
endTime: timeEntry.endTime ? new Date(timeEntry.endTime) : null,
duration: timeEntry.duration
? timeEntry.duration
- : timeEntry.endTime
+ : timeEntry.endTime && timeEntry.startTime
? new Date(timeEntry.endTime).getTime() -
new Date(timeEntry.startTime).getTime()
: null
diff --git a/app/routes/reports.tsx b/app/routes/reports.tsx
index 5163f92..18e69c8 100644
--- a/app/routes/reports.tsx
+++ b/app/routes/reports.tsx
@@ -17,7 +17,7 @@ import {
updateDuration
} from '~/models/timeEntry.server';
import { getProjects, Project } from '~/models/project.server';
-import { requireUserId } from '~/session.server';
+import { requireUser } from '~/session.server';
import { DateRangePicker, DateRangePickerValue } from '@mantine/dates';
import { useEffect, useState } from 'react';
import dayjs from 'dayjs';
@@ -35,8 +35,8 @@ export const meta: MetaFunction = () => {
};
export async function loader({ request }: LoaderArgs) {
- const userId = await requireUserId(request);
- if (!userId) return redirect('/login');
+ const user = await requireUser(request);
+ if (!user) return redirect('/login');
const url = new URL(request.url);
const dateFrom = url.searchParams.get('dateFrom')
@@ -46,19 +46,21 @@ export async function loader({ request }: LoaderArgs) {
? dayjs(url.searchParams.get('dateTo')).endOf('day').toDate()
: dayjs().endOf('month').endOf('day').toDate();
- await updateDuration(userId);
+ await updateDuration(user.id);
return json({
+ user,
timeByProject: await getTimeEntriesByDateAndProject({
- userId,
+ userId: user.id,
dateFrom,
dateTo
}),
- projects: await getProjects({ userId })
+ projects: await getProjects({ userId: user.id })
});
}
export default function ReportPage() {
+ const data = useLoaderData();
const reports = useFetcher();
const [dateRange, setDateRange] = useState([
@@ -76,7 +78,9 @@ export default function ReportPage() {
}
}, [dateRange]);
- const [costPerHour, setCostPerHour] = useState();
+ const [hourlyRate, setHourlyRate] = useState(
+ data.user.defaultHourlyRate || undefined
+ );
const mobile = useMediaQuery('(max-width: 600px)');
@@ -99,7 +103,14 @@ export default function ReportPage() {
-
+
@@ -218,7 +229,7 @@ export default function ReportPage() {
| Project |
Time |
- {costPerHour && Billing | }
+ {hourlyRate && Billing | }
@@ -247,15 +258,15 @@ export default function ReportPage() {
{(projectData._sum.duration / 1000 / 60 / 60).toFixed(2)} h
|
- {costPerHour && (
+ {hourlyRate && (
{(
- (projectData._sum.duration * costPerHour) /
+ (projectData._sum.duration * hourlyRate) /
1000 /
60 /
60
).toFixed(2)}{' '}
- €
+ {reports.data?.user?.currency ?? '€'}
|
)}
diff --git a/prisma/migrations/20230301010453_add_userprefs_currency_hourlyrate/migration.sql b/prisma/migrations/20230301010453_add_userprefs_currency_hourlyrate/migration.sql
new file mode 100644
index 0000000..c9261eb
--- /dev/null
+++ b/prisma/migrations/20230301010453_add_userprefs_currency_hourlyrate/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "defaultCurrency" TEXT NOT NULL DEFAULT '€',
+ADD COLUMN "defaultHourlyRate" DOUBLE PRECISION;
diff --git a/prisma/migrations/20230301010640_rename_currency_userpref/migration.sql b/prisma/migrations/20230301010640_rename_currency_userpref/migration.sql
new file mode 100644
index 0000000..1fce1c7
--- /dev/null
+++ b/prisma/migrations/20230301010640_rename_currency_userpref/migration.sql
@@ -0,0 +1,9 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `defaultCurrency` on the `User` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "defaultCurrency",
+ADD COLUMN "currency" TEXT NOT NULL DEFAULT '€';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index dbd4e0c..891e4fa 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -12,7 +12,9 @@ model User {
email String @unique
admin Boolean @default(false)
- dateFormat String @default("en-GB")
+ dateFormat String @default("en-GB")
+ currency String @default("€")
+ defaultHourlyRate Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt