Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions web/src/components/entry-dialog/EntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,21 @@ export type EntryFormProps = {
type LocationState = {
date?: string;
editEntry?: Entry;
template?: Entry;
templateEntries?: Entry[];
};

const EntryForm = () => {
const { state } = useLocation();
const dayjs = useDayjs();
// state is possibly null
const { date: originalDate, editEntry }: LocationState = state || {};
const { form, onSubmit, loading } = useEntryForm({ editEntry, date: dayjs(originalDate) });
const { date: originalDate, editEntry, templateEntries }: LocationState = state || {};

const { form, onSubmit, loading } = useEntryForm({
editEntry,
date: dayjs(originalDate),
template: templateEntries && templateEntries[0],
});
const navigate = useNavigate();

const {
Expand All @@ -62,10 +69,25 @@ const EntryForm = () => {

useEffect(() => {
if (isSubmitSuccessful) {
reset();
navigate("..");
const remainingEntries = templateEntries && templateEntries.slice(1);
if (remainingEntries && remainingEntries.length > 0) {
const nextEntry = remainingEntries[0];
reset({
date: dayjs(originalDate),
duration: nextEntry.duration.toString(),
description: nextEntry.description || "",
product: nextEntry.product || "",
activity: nextEntry.activity || "",
issue: nextEntry.issue || null,
client: nextEntry.client || "",
});
navigate(".", { state: { date: originalDate, templateEntries: remainingEntries } });
} else {
reset();
navigate("..");
}
}
}, [isSubmitSuccessful, navigate, reset]);
}, [dayjs, isSubmitSuccessful, navigate, originalDate, reset, templateEntries]);

const { t } = useTranslation();
const theme = useTheme();
Expand Down Expand Up @@ -197,7 +219,7 @@ const EntryForm = () => {
)}
</Grid>
<Grid item xs={12}>
{!editEntry && (
{!editEntry && !templateEntries && (
<FormGroup>
<FormControlLabel
control={
Expand All @@ -217,7 +239,7 @@ const EntryForm = () => {
<>
<Grid item xs={4} sx={{ mt: 2 }}>
<Box sx={{ display: "flex", justifyContent: "start", gap: 2 }}>
{!editEntry && (
{!editEntry && !templateEntries && (
<FormGroup>
<FormControlLabel
control={
Expand Down
22 changes: 14 additions & 8 deletions web/src/components/entry-dialog/useEntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type EntryFormSchema = {
export type useEntryProps = {
editEntry?: Entry;
date?: Dayjs;
template?: Entry;
};

/**
Expand All @@ -34,7 +35,7 @@ export type useEntryProps = {
* Note that calling this hook will create a new RHF form instance, i.e., you must pass down
* the actual instance from parent components rather than call this hook again in child components.
*/
const useEntryForm = ({ editEntry, date }: useEntryProps) => {
const useEntryForm = ({ editEntry, date, template }: useEntryProps) => {
const { t } = useTranslation();
const { showSuccessNotification } = useNotification();
const dayjs = useDayjs();
Expand Down Expand Up @@ -68,12 +69,17 @@ const useEntryForm = ({ editEntry, date }: useEntryProps) => {

return {
date: date ? dayjs(date) : dayjs(),
duration: editEntry?.duration.toString() || "",
description: editEntry?.description || "",
product: editEntry?.product || settingsData?.getMySettings.productPreset || "",
activity: editEntry?.activity || settingsData?.getMySettings.activityPreset || "",
issue: editEntry?.issue || null,
client: editEntry?.client || "",
duration: editEntry?.duration.toString() || template?.duration.toString() || "",
description: editEntry?.description || template?.description || "",
product:
editEntry?.product || template?.product || settingsData?.getMySettings.productPreset || "",
activity:
editEntry?.activity ||
template?.activity ||
settingsData?.getMySettings.activityPreset ||
"",
issue: editEntry?.issue || template?.issue || null,
client: editEntry?.client || template?.client || "",
};
};

Expand All @@ -83,7 +89,7 @@ const useEntryForm = ({ editEntry, date }: useEntryProps) => {

const { loading: hoursLoading } = useFormSetRemainingHours({
form,
isEnabled: editEntry === undefined && !form.formState.isLoading,
isEnabled: editEntry === undefined && template === undefined && !form.formState.isLoading,
});

const addWorkday: SubmitHandler<EntryFormSchema> = async (formValues) => {
Expand Down
37 changes: 37 additions & 0 deletions web/src/components/workday-accordion/PasteEditEntryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import SaveAsIcon from "@mui/icons-material/SaveAs";
import { Box, IconButtonProps } from "@mui/material";
import { useTranslation } from "react-i18next";
import LabelledIconButton from "../LabelledIconButton";
import { useLocation, useNavigate } from "react-router-dom";
import { Dayjs } from "dayjs";
import { useEntryContext } from "../workday-browser/entry-context/useEntryContext";

type PasteEntryButtonProps = { date: Dayjs } & IconButtonProps;

const PasteEditEntryButton = ({ date, ...props }: PasteEntryButtonProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const { selectedEntries, clearEntries } = useEntryContext();

return (
<Box onClick={(e) => e.stopPropagation()}>
<LabelledIconButton
size="medium"
label={t("controls.pasteAndEditEntry")}
onClick={() => {
const templateEntries = selectedEntries;
clearEntries();
navigate(`${location.pathname}/create`, {
state: { date: date.format("YYYY-MM-DD"), templateEntries },
});
}}
{...props}
>
<SaveAsIcon fontSize="inherit" />
</LabelledIconButton>
</Box>
);
};

export default PasteEditEntryButton;
20 changes: 20 additions & 0 deletions web/src/components/workday-accordion/PasteEntryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ContentPasteGoIcon from "@mui/icons-material/ContentPasteGo";
import { Box, IconButtonProps } from "@mui/material";
import { useTranslation } from "react-i18next";
import LabelledIconButton from "../LabelledIconButton";

type PasteEntryButtonProps = IconButtonProps;

const PasteEntryButton = ({ ...props }: PasteEntryButtonProps) => {
const { t } = useTranslation();

return (
<Box onClick={(e) => e.stopPropagation()}>
<LabelledIconButton size="medium" label={t("controls.pasteEntry")} {...props}>
<ContentPasteGoIcon fontSize="inherit" />
</LabelledIconButton>
</Box>
);
};

export default PasteEntryButton;
52 changes: 51 additions & 1 deletion web/src/components/workday-accordion/WorkdaySummary.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { AccordionSummary, Box, Chip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { roundToFullMinutes, totalDurationOfEntries } from "../../common/duration";
import useDayjs from "../../common/useDayjs";
import {
Expand All @@ -10,14 +12,23 @@ import {
isVacation,
isWeekend,
} from "../../common/workdayUtils";
import { Workday } from "../../graphql/generated/graphql";
import {
AddWorkdayEntryDocument,
Entry,
FindWorkdaysDocument,
Workday,
} from "../../graphql/generated/graphql";
import EntryDialogButton from "../entry-dialog/EntryDialogButton";
import FlexLeaveChip from "./info-chips/FlexLeaveChip";
import HolidayChip from "./info-chips/HolidayChip";
import NoEntriesChip from "./info-chips/NoEntriesChip";
import SickLeaveChip from "./info-chips/SickLeaveChip";
import VacationChip from "./info-chips/VacationChip";
import WeekendChip from "./info-chips/WeekendChip";
import { useEntryContext } from "../workday-browser/entry-context/useEntryContext";
import { useNotification } from "../global-notification/useNotification";
import PasteEntryButton from "./PasteEntryButton";
import PasteEditEntryButton from "./PasteEditEntryButton";

type WorkdayAccordionProps = {
workday: Workday;
Expand All @@ -39,6 +50,34 @@ const WorkdaySummary = ({ workday }: WorkdayAccordionProps) => {
const totalHoursFormatted = roundToFullMinutes(totalDuration).format("H:mm");

const empty = workday.entries.length === 0;
const { t } = useTranslation();

const { selectedEntries, hasEntries, clearEntries } = useEntryContext();
const { showSuccessNotification } = useNotification();
const [addWorkdayEntryMutation] = useMutation(AddWorkdayEntryDocument, {
refetchQueries: [FindWorkdaysDocument],
onCompleted: async () => {
showSuccessNotification(t("notifications.addEntry.success"));
},
});
const handlePasteEntries = (entries: Entry[]) => {
entries.forEach((entry) => {
addWorkdayEntryMutation({
variables: {
entry: {
date: date.format("YYYY-MM-DD"),
duration: entry.duration,
description: entry.description,
product: entry.product,
activity: entry.activity,
issue: entry.issue,
client: entry.client,
},
},
});
});
clearEntries();
};

const InfoChip = () => {
if (vacation) {
Expand Down Expand Up @@ -88,6 +127,17 @@ const WorkdaySummary = ({ workday }: WorkdayAccordionProps) => {
</Box>
{!mobile && <InfoChip />}
<Box sx={{ display: "flex", alignItems: "center" }}>
{hasEntries ? (
<>
<PasteEditEntryButton date={date} />
<PasteEntryButton
onClick={(e) => {
e.stopPropagation();
handlePasteEntries(selectedEntries);
}}
/>
</>
) : null}
{!disabled && (
<>
<EntryDialogButton date={date} size="medium" />
Expand Down
43 changes: 43 additions & 0 deletions web/src/components/workday-accordion/entry-row/CopyEntryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { ToggleButton, Tooltip } from "@mui/material";
import { useTranslation } from "react-i18next";
import { Entry } from "../../../graphql/generated/graphql";
import { useEntryContext } from "../../workday-browser/entry-context/useEntryContext";

type CopyEntryButtonProps = {
entry: Entry;
};

const CopyEntryButton = ({ entry }: CopyEntryButtonProps) => {
const { t } = useTranslation();
const { hasEntry, addSelectedEntry, removeSelectedEntry } = useEntryContext();

return (
<>
<Tooltip title={t("controls.copyEntry")} arrow placement="bottom">
<ToggleButton
value={t("controls.copyEntry")}
aria-label={t("controls.copyEntry")}
size="medium"
onClick={(e) => {
e.stopPropagation();
!hasEntry(entry) ? addSelectedEntry(entry) : removeSelectedEntry(entry);
}}
sx={(theme) => ({
color:
theme.palette.mode === "dark"
? theme.palette.primary.main
: theme.palette.secondary.dark,
border: "none",
borderRadius: "50%",
})}
selected={hasEntry(entry)}
>
<ContentCopyIcon fontSize="small" />
</ToggleButton>
</Tooltip>
</>
);
};

export default CopyEntryButton;
12 changes: 12 additions & 0 deletions web/src/components/workday-accordion/entry-row/DesktopEntryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { EntryRowProps } from "./EntryRow";
import AcceptedChip from "./status-chips/AcceptedChip";
import OpenChip from "./status-chips/OpenChip";
import PaidChip from "./status-chips/PaidChip";
import { useEntryContext } from "../../workday-browser/entry-context/useEntryContext";
import CopyEntryButton from "./CopyEntryButton";

const DesktopEntryRow = ({ entry, date }: EntryRowProps) => {
const { darkMode } = useDarkMode();
Expand All @@ -19,6 +21,7 @@ const DesktopEntryRow = ({ entry, date }: EntryRowProps) => {
const paid = entry.acceptanceStatus === AcceptanceStatus.Paid;
const open = entry.acceptanceStatus === AcceptanceStatus.Open;
const roundedDuration = roundToFullMinutes(dayjs.duration(entry.duration, "hour"));
const { hasEntry } = useEntryContext();

return (
<ListItem
Expand All @@ -33,6 +36,12 @@ const DesktopEntryRow = ({ entry, date }: EntryRowProps) => {
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
backgroundColor: (theme) =>
hasEntry(entry)
? darkMode
? theme.palette.grey[700]
: theme.palette.primary.main
: undefined,
}}
>
<Box
Expand Down Expand Up @@ -85,6 +94,9 @@ const DesktopEntryRow = ({ entry, date }: EntryRowProps) => {
</Box>
) : (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box>
<CopyEntryButton entry={entry} />
</Box>
<Box>
<EditEntryButton date={date} entry={entry} />
</Box>
Expand Down
10 changes: 10 additions & 0 deletions web/src/components/workday-accordion/entry-row/MobileEntryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { EntryRowProps } from "./EntryRow";
import AcceptedChip from "./status-chips/AcceptedChip";
import OpenChip from "./status-chips/OpenChip";
import PaidChip from "./status-chips/PaidChip";
import CopyEntryButton from "./CopyEntryButton";
import { useEntryContext } from "../../workday-browser/entry-context/useEntryContext";

const MobileEntryRow = ({ entry, date }: EntryRowProps) => {
const { darkMode } = useDarkMode();
Expand All @@ -18,6 +20,7 @@ const MobileEntryRow = ({ entry, date }: EntryRowProps) => {
const paid = entry.acceptanceStatus === AcceptanceStatus.Paid;
const open = entry.acceptanceStatus === AcceptanceStatus.Open;
const roundedDuration = roundToFullMinutes(dayjs.duration(entry.duration, "hour"));
const { hasEntry } = useEntryContext();

return (
<ListItem
Expand All @@ -32,6 +35,12 @@ const MobileEntryRow = ({ entry, date }: EntryRowProps) => {
display: "flex",
flexDirection: "column",
alignItems: "stretch",
backgroundColor: (theme) =>
hasEntry(entry)
? darkMode
? theme.palette.grey[700]
: theme.palette.primary.main
: undefined,
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between", minHeight: 40 }}>
Expand All @@ -58,6 +67,7 @@ const MobileEntryRow = ({ entry, date }: EntryRowProps) => {
) : (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box>
<CopyEntryButton entry={entry} />
<EditEntryButton date={date} entry={entry} />
</Box>
</Box>
Expand Down
Loading