feat: add currency + hourly rate to user, fixes for import and reports
This commit is contained in:
parent
1a57580b6e
commit
01ec528ee4
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "defaultCurrency" TEXT NOT NULL DEFAULT '€',
|
||||
ADD COLUMN "defaultHourlyRate" DOUBLE PRECISION;
|
||||
|
|
@ -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 '€';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue