Skip to content

Commit

Permalink
✨ Add Adjustable Time Reporting feature
Browse files Browse the repository at this point in the history
  • Loading branch information
karlosos committed Jan 11, 2024
1 parent 102f488 commit 29fb56d
Show file tree
Hide file tree
Showing 13 changed files with 560 additions and 22 deletions.
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@reduxjs/toolkit": "^1.9.5",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
Expand Down
35 changes: 35 additions & 0 deletions src/app/features/Settings/ExperimentalFeatures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DialogContentText } from "@mui/material";
import { useAppDispatch, useAppSelector } from "../../hooks";
import { RootState } from "../../store/store";
import { Switch } from "../../ui/Switch";
import { setAdjustableTimeReporting } from "./slice";

export const ExperimentalFeatures = () => {
const dispatch = useAppDispatch();
const isAdjustableTimeReportingEnabled = useAppSelector(
(state: RootState) =>
state.settings.featureFlags.isAdjustableTimeReportingEnabled
);

return (
<>
<DialogContentText>Experimental features</DialogContentText>
<div className="mt-2 flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<div className="font-medium">Adjustable time reporting</div>
<div className="text-[#666666]">
Accommodating situations where the officially reported time may
differ from the actual time spend on a task.
</div>
</div>
<Switch
checked={isAdjustableTimeReportingEnabled}
onCheckedChange={(checked) =>
dispatch(setAdjustableTimeReporting(checked))
}
className="h-5 w-9"
/>
</div>
</>
);
};
3 changes: 3 additions & 0 deletions src/app/features/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DialogTitle,
DialogTrigger,
} from "../../ui/Dialog";
import { ExperimentalFeatures } from "./ExperimentalFeatures";
import { ImportExport } from "./ImportExport";
import { LinkPatterns } from "./LinkPatterns";

Expand All @@ -23,6 +24,8 @@ export const Settings = () => {
<div>
<LinkPatterns />
<hr className="my-4" />
<ExperimentalFeatures />
<hr className="my-4" />
<ImportExport closeSettingsDialog={() => setIsDialogOpen(false)} />
</div>
</DialogContent>
Expand Down
11 changes: 10 additions & 1 deletion src/app/features/Settings/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export type LinkPattern = {

export type SettingsState = {
patterns: LinkPattern[];
featureFlags: {
isAdjustableTimeReportingEnabled: boolean;
};
};

export const settingsInitialState: SettingsState = {
Expand All @@ -34,6 +37,9 @@ export const settingsInitialState: SettingsState = {
url: "https://jiradc.ext.net.nokia.com/browse/",
},
],
featureFlags: {
isAdjustableTimeReportingEnabled: false,
},
};

export const settings = createSlice({
Expand All @@ -43,6 +49,9 @@ export const settings = createSlice({
patternsChanged: (state, action: PayloadAction<LinkPattern[]>) => {
state.patterns = action.payload;
},
setAdjustableTimeReporting: (state, action: PayloadAction<boolean>) => {
state.featureFlags.isAdjustableTimeReportingEnabled = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(clearAppState, (state, action) => {
Expand All @@ -54,7 +63,7 @@ export const settings = createSlice({
},
});

export const { patternsChanged } = settings.actions;
export const { patternsChanged, setAdjustableTimeReporting } = settings.actions;

export default settings.reducer;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Checkbox } from "@mui/material";
import { formatElapsedTime } from "../../../utils";
import { PlayCircle } from "@mui/icons-material";
import { useAppDispatch } from "../../../hooks";
import { useAppDispatch, useAppSelector } from "../../../hooks";
import { TimeEntryRow } from "./TimeEntryRow";
import { useState } from "react";
import { useDispatch } from "react-redux";
Expand All @@ -13,6 +13,8 @@ import {
timeEntryAdded,
} from "../store";
import { Button } from "../../../ui/Button";
import { TimeReportingDialog } from "./TimeReportingDialog";
import { RootState } from "../../../store/store";

interface GroupedTimeEntryRowProps {
groupedTimeEntry: GroupedTimeEntry;
Expand All @@ -23,6 +25,12 @@ export const GroupedTimeEntryRow: React.FC<GroupedTimeEntryRowProps> = ({
}) => {
const dispatch = useAppDispatch();
const [isCollapsed, setIsCollapsed] = useState(true);
const [isTimeReportingDialogVisible, setIsTimeReportingDialogVisible] =
useState(false);
const isAdjustableTimeReportingEnabled = useAppSelector(
(state: RootState) =>
state.settings.featureFlags.isAdjustableTimeReportingEnabled
);

const handleAddTimeEntryClick = () => {
dispatch(
Expand Down Expand Up @@ -56,12 +64,43 @@ export const GroupedTimeEntryRow: React.FC<GroupedTimeEntryRowProps> = ({
checked={checkboxState}
indeterminate={checkboxIsIndeterminate}
disabled={checkboxIsIndeterminate}
onChange={handleCheckboxChange}
onChange={(e) => {
if (isAdjustableTimeReportingEnabled) {
setIsTimeReportingDialogVisible(true);
} else {
handleCheckboxChange(e);
}
}}
aria-label="is logged status"
/>
<div className="w-[65px] text-center text-sm font-medium text-neutral-800 opacity-60">
{formatElapsedTime(groupedTimeEntry.elapsedTime)}
</div>
{isAdjustableTimeReportingEnabled ? (
<div
className="flex w-[65px] cursor-default flex-col items-center justify-center"
onClick={() => {
setIsTimeReportingDialogVisible(true);
}}
>
<div className="rounded rounded-b-none border border-b-0 px-2 text-center text-xs font-medium tabular-nums text-neutral-800 opacity-60">
{formatElapsedTime(groupedTimeEntry.elapsedTime)}
</div>

<div className="flex items-center text-xs font-medium ">
<span className="flex items-center rounded rounded-t-none border bg-neutral-100 px-2 tabular-nums text-neutral-700 opacity-50">
{formatElapsedTime(
groupedTimeEntry.subEntries.reduce(
(sum, entry) =>
sum + (entry.logged ? entry.loggedTime ?? 0 : 0),
0
)
)}
</span>
</div>
</div>
) : (
<div className="w-[65px] text-center text-sm font-medium text-neutral-800 opacity-60">
{formatElapsedTime(groupedTimeEntry.elapsedTime)}
</div>
)}
<ToggleAccordionIcon
onClick={handleToggleCollapse}
aria-label="Grouped entry accordion"
Expand All @@ -76,6 +115,15 @@ export const GroupedTimeEntryRow: React.FC<GroupedTimeEntryRowProps> = ({
</Button>
</div>
</div>

{isTimeReportingDialogVisible && (
<TimeReportingDialog
groupedTimeEntry={groupedTimeEntry}
setIsVisible={setIsTimeReportingDialogVisible}
isVisible={isTimeReportingDialogVisible}
/>
)}

{!isCollapsed && (
<div className="flex flex-col">
{[...groupedTimeEntry.subEntries].reverse().map((entry) => (
Expand Down
69 changes: 55 additions & 14 deletions src/app/features/TimeEntries/TimeEntriesList/TimeEntriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
selectTimeEntriesCount,
} from "../store";
import { Button } from "../../../ui/Button";
import { RootState } from "../../../store/store";

const TIME_ENTRIES_LIMIT = 50;

Expand All @@ -28,17 +29,28 @@ export const TimeEntriesList = () => {
return (
<div className="mt-4 flex flex-col space-y-6">
{sortedTimeEntries.map(([date, groupedTimeEntriesPerDate]) => {
const elapsedTimePerDay = groupedTimeEntriesPerDate.reduce(
(acc: number, groupedTimeEntries) =>
acc + groupedTimeEntries.elapsedTime,
0
);
const [elapsedTimePerDay, reportedTimePerDay] =
groupedTimeEntriesPerDate.reduce(
(acc: number[], groupedTimeEntries) => [
acc[0] + groupedTimeEntries.elapsedTime,
acc[1] +
groupedTimeEntries.subEntries.reduce(
(sum, e) => sum + (e.logged ? e.loggedTime ?? 0 : 0),
0
),
],
[0, 0]
);
return (
<div
key={date}
className="rounded-lg border p-4 shadow-[-2px_5px_20px_0px_#0000001A]"
>
<DayHeader date={date} elapsedTimePerDay={elapsedTimePerDay} />
<DayHeader
date={date}
elapsedTimePerDay={elapsedTimePerDay}
reportedTimePerDay={reportedTimePerDay}
/>
{groupedTimeEntriesPerDate.map((groupedTimeEntries) => (
<GroupedTimeEntryRow
groupedTimeEntry={groupedTimeEntries}
Expand All @@ -60,19 +72,48 @@ export const TimeEntriesList = () => {
function DayHeader({
date,
elapsedTimePerDay,
reportedTimePerDay,
}: {
date: string;
elapsedTimePerDay: number;
reportedTimePerDay: number;
}) {
return (
<div>
<span className="text-lg font-semibold text-neutral-700">{date}</span>{" "}
&nbsp;
<span className="text-lg font-semibold text-neutral-700 opacity-50">
{formatElapsedTime(elapsedTimePerDay)}
</span>
</div>
const isAdjustableTimeReportingEnabled = useAppSelector(
(state: RootState) =>
state.settings.featureFlags.isAdjustableTimeReportingEnabled
);

if (isAdjustableTimeReportingEnabled) {
return (
<div className="flex items-center">
<span className="mr-2 text-lg font-semibold text-neutral-700">
{date}
</span>
<span className="mr-2 text-lg font-semibold text-neutral-700 opacity-50">
{formatElapsedTime(elapsedTimePerDay)}
</span>

<div className="flex items-center text-xs font-semibold">
<span className="rounded rounded-r-none border border-neutral-500 bg-neutral-500 pl-2 pr-1 text-white">
Logged
</span>
<span className="flex items-center rounded rounded-l-none border bg-neutral-100 pl-1 pr-2 text-neutral-700 opacity-50">
{formatElapsedTime(reportedTimePerDay)}
</span>
</div>
</div>
);
} else {
return (
<div>
<span className="text-lg font-semibold text-neutral-700">{date}</span>{" "}
&nbsp;
<span className="text-lg font-semibold text-neutral-700 opacity-50">
{formatElapsedTime(elapsedTimePerDay)}
</span>
</div>
);
}
}

type PaginationButtonsProps = {
Expand Down
Loading

0 comments on commit 29fb56d

Please sign in to comment.