feat: add currency + hourly rate to user, fixes for import and reports

This commit is contained in:
Nicola Zambello 2023-03-01 02:27:43 +01:00
parent 1a57580b6e
commit 01ec528ee4
Signed by: nzambello
GPG key ID: 56E4A92C2C1E50BA
6 changed files with 98 additions and 37 deletions

View file

@ -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 (
<Box sx={{ maxWidth: 300 }} mx="auto">
<Title order={2} my="lg">
@ -188,23 +211,35 @@ export default function Account() {
])}
/>
<p>Example:</p>
<blockquote>
{isHydrated && (
<p>
Example:{' '}
{Intl.DateTimeFormat(dateFormat, {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(new Date(Date.now()))}
<br />
{Intl.DateTimeFormat(dateFormat, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(Date.now()))}
</blockquote>
</p>
)}
<Group position="center" mt="sm">
<TextInput
name="currency"
label="Currency"
placeholder="Select your currency"
defaultValue={loaderData.user.currency}
mb="lg"
/>
<NumberInput
name="defaultHourlyRate"
label="Hourly rate"
placeholder="Enter your hourly rate"
defaultValue={loaderData.user.defaultHourlyRate || undefined}
min={0}
mb="lg"
/>
<Group position="center" mt="lg">
<Button type="submit" leftIcon={<Save size={14} />}>
Save
</Button>

View file

@ -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

View file

@ -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<typeof loader>();
const reports = useFetcher<typeof loader>();
const [dateRange, setDateRange] = useState<DateRangePickerValue>([
@ -76,7 +78,9 @@ export default function ReportPage() {
}
}, [dateRange]);
const [costPerHour, setCostPerHour] = useState<number>();
const [hourlyRate, setHourlyRate] = useState<number | undefined>(
data.user.defaultHourlyRate || undefined
);
const mobile = useMediaQuery('(max-width: 600px)');
@ -99,7 +103,14 @@ export default function ReportPage() {
</h1>
<reports.Form action="/reports" method="get">
<Paper p="sm" shadow="sm" radius="md" component="fieldset">
<Paper
p="sm"
aria-controls="time-entries"
shadow="sm"
radius="md"
withBorder
component="fieldset"
>
<DateRangePicker
label="Select date range"
placeholder="Pick dates range"
@ -206,9 +217,9 @@ export default function ReportPage() {
<Box mt="md" maw={300}>
<NumberInput
label="Cost per hour"
value={costPerHour}
onChange={setCostPerHour}
label="Hourly rate"
value={hourlyRate || data.user.defaultHourlyRate || undefined}
onChange={setHourlyRate}
/>
</Box>
@ -218,7 +229,7 @@ export default function ReportPage() {
<tr>
<th>Project</th>
<th>Time</th>
{costPerHour && <th>Billing</th>}
{hourlyRate && <th>Billing</th>}
</tr>
</thead>
<tbody>
@ -247,15 +258,15 @@ export default function ReportPage() {
<td>
{(projectData._sum.duration / 1000 / 60 / 60).toFixed(2)} h
</td>
{costPerHour && (
{hourlyRate && (
<td>
{(
(projectData._sum.duration * costPerHour) /
(projectData._sum.duration * hourlyRate) /
1000 /
60 /
60
).toFixed(2)}{' '}
{reports.data?.user?.currency ?? '€'}
</td>
)}
</tr>

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "defaultCurrency" TEXT NOT NULL DEFAULT '',
ADD COLUMN "defaultHourlyRate" DOUBLE PRECISION;

View file

@ -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 '';

View file

@ -13,6 +13,8 @@ model User {
admin Boolean @default(false)
dateFormat String @default("en-GB")
currency String @default("€")
defaultHourlyRate Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt