Skip to content

Commit 064e8c8

Browse files
authored
Feat/ranged view (#52)
* Add `GetWorkTimeForRange` & `GetProjectWorkTimeForRange` methods - `GetWorkTimeForRange` will return a map of the total sum for all projects worked in that range as well as the totals for each individual project - `GetProjectWorkTimeForRange` will just return the total sum for the specific project within that range * Create `RangeView` component - Basically just a quick form to be able to see the totals for the wanted range without the limitations the other pages have for per week and per month * Update RangeView.tsx - cleanup
1 parent 3b7e56f commit 064e8c8

File tree

6 files changed

+288
-23
lines changed

6 files changed

+288
-23
lines changed

db.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,78 @@ func (a *App) GetWorkTime(date string, organizationID uint) (seconds int, err er
470470
return totalSeconds, nil
471471
}
472472

473+
// GetWorkTimeForRange(startDate, endDate, organizationID)
474+
func (a *App) GetWorkTimeForRange(startDate, endDate string, organizationID uint) (workTimes map[string]int, err error) {
475+
if startDate == "" || endDate == "" || organizationID == 0 {
476+
return nil, nil
477+
}
478+
479+
organization, err := a.getOrganization(organizationID)
480+
if err != nil {
481+
Logger.Println(err)
482+
return nil, err
483+
}
484+
485+
totalSeconds := 0
486+
workTimes = make(map[string]int)
487+
488+
// Query to get the total work time for each project within the given date range
489+
rows, err := a.db.Model(&WorkHours{}).
490+
Select("projects.name, COALESCE(SUM(work_hours.seconds), 0) as total_seconds").
491+
Joins("JOIN projects ON projects.id = work_hours.project_id").
492+
Where("projects.organization_id = ? AND projects.deleted_at IS NULL", organization.ID).
493+
Where("work_hours.date >= ? AND work_hours.date <= ?", startDate, endDate).
494+
Group("projects.name").
495+
Rows()
496+
if err != nil {
497+
Logger.Println(err)
498+
return nil, err
499+
}
500+
defer rows.Close()
501+
502+
// Iterate over the rows and populate the map
503+
for rows.Next() {
504+
var projectName string
505+
var projectSeconds int
506+
if err := rows.Scan(&projectName, &projectSeconds); err != nil {
507+
Logger.Println(err)
508+
return nil, err
509+
}
510+
workTimes[projectName] = projectSeconds
511+
totalSeconds += projectSeconds
512+
}
513+
workTimes["total"] = totalSeconds
514+
515+
return workTimes, nil
516+
}
517+
518+
// GetProjectWorkTimeForRange(startDate, endDate, projectID) (seconds, err)
519+
func (a *App) GetProjectWorkTimeForRange(startDate, endDate string, projectID uint) (seconds int, err error) {
520+
if startDate == "" || endDate == "" || projectID == 0 {
521+
return 0, nil
522+
}
523+
524+
// Find the project
525+
project, err := a.getProject(projectID)
526+
if err != nil {
527+
Logger.Println(err)
528+
return 0, err
529+
}
530+
531+
// Get the total work time for the project within the given date range
532+
var totalSeconds int
533+
err = a.db.Model(&WorkHours{}).
534+
Where("project_id = ? AND date >= ? AND date <= ?", project.ID, startDate, endDate).
535+
Select("COALESCE(SUM(seconds), 0)").
536+
Row().Scan(&totalSeconds)
537+
if err != nil {
538+
Logger.Println(err, totalSeconds)
539+
return 0, err
540+
}
541+
542+
return totalSeconds, nil
543+
}
544+
473545
// GetDailyWorkTime returns the total seconds worked for each day for the specified organization
474546
// func (a *App) GetDailyWorkTime(organizationName string) (dailyWorkTime map[string]int, err error) {
475547
// dailyWorkTime = make(map[string]int)

frontend/src/components/RangeView.tsx

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useAppStore } from "@/stores/main";
2+
import { formatTime } from "@/utils/utils";
3+
import { GetWorkTimeForRange } from "@go/main/App";
4+
import {
5+
Box,
6+
Button,
7+
Dialog,
8+
DialogActions,
9+
DialogContent,
10+
DialogContentText,
11+
DialogTitle,
12+
FormControl,
13+
Grid,
14+
InputLabel,
15+
MenuItem,
16+
Select,
17+
TextField,
18+
Typography,
19+
} from "@mui/material";
20+
import { useState } from "react";
21+
22+
interface RangeViewProps {
23+
openRangeView: boolean;
24+
setOpenRangeView: (value: boolean) => void;
25+
}
26+
27+
const RangeView: React.FC<RangeViewProps> = ({ openRangeView, setOpenRangeView }) => {
28+
const today = new Date().toISOString().split("T")[0];
29+
const orgs = useAppStore((state) => state.organizations);
30+
const [startDate, setStartDate] = useState("");
31+
const [endDate, setEndDate] = useState("");
32+
const [selectedOrg, setSelectedOrg] = useState<number | null>(null);
33+
const [workTimes, setWorkTimes] = useState<{ [key: string]: number } | null>(null);
34+
35+
const handleStartDateChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
36+
setStartDate(e.target.value);
37+
};
38+
39+
const handleEndDateChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
40+
setEndDate(e.target.value);
41+
};
42+
43+
const handleFetchWorkTime = () => {
44+
if (!startDate || !endDate || !selectedOrg) return;
45+
GetWorkTimeForRange(startDate, endDate, selectedOrg).then(setWorkTimes).catch(console.error);
46+
};
47+
48+
return (
49+
<Dialog
50+
disableEscapeKeyDown
51+
fullWidth
52+
fullScreen
53+
open={openRangeView}
54+
onClose={() => setOpenRangeView(false)}
55+
>
56+
<DialogTitle>Get Work totals for a date range</DialogTitle>
57+
<DialogContent>
58+
<DialogContentText>Select Start and End dates and a Organization</DialogContentText>
59+
<Box sx={{ p: 3 }}>
60+
<Grid
61+
container
62+
spacing={2}
63+
>
64+
<Grid
65+
item
66+
xs={12}
67+
sm={6}
68+
>
69+
<TextField
70+
label="Start Date"
71+
type="date"
72+
value={startDate}
73+
onChange={handleStartDateChange}
74+
fullWidth
75+
slotProps={{
76+
inputLabel: { shrink: true },
77+
}}
78+
/>
79+
</Grid>
80+
<Grid
81+
item
82+
xs={12}
83+
sm={6}
84+
>
85+
<TextField
86+
label="End Date"
87+
type="date"
88+
value={endDate}
89+
onChange={handleEndDateChange}
90+
fullWidth
91+
slotProps={{
92+
inputLabel: { shrink: true },
93+
htmlInput: { max: today },
94+
}}
95+
/>
96+
</Grid>
97+
<Grid
98+
item
99+
xs={12}
100+
>
101+
<FormControl fullWidth>
102+
<InputLabel id="select-organization-label">Select Organization</InputLabel>
103+
<Select
104+
label="Select Organization"
105+
labelId="select-organization-label"
106+
value={selectedOrg}
107+
onChange={(event) => setSelectedOrg(event.target.value as number)}
108+
displayEmpty
109+
>
110+
<MenuItem
111+
value=""
112+
disabled
113+
>
114+
Select Organization
115+
</MenuItem>
116+
{orgs.map((org) => (
117+
<MenuItem
118+
key={org.id}
119+
value={org.id}
120+
>
121+
{org.name}
122+
</MenuItem>
123+
))}
124+
</Select>
125+
</FormControl>
126+
</Grid>
127+
</Grid>
128+
</Box>
129+
{workTimes && (
130+
<Box sx={{ mt: 4 }}>
131+
<Typography variant="h6">Work Time Results</Typography>
132+
<Typography variant="body1">
133+
<strong>Total Work Time:</strong> {formatTime(workTimes.total)} seconds
134+
</Typography>
135+
<Box sx={{ mt: 2 }}>
136+
{Object.entries(workTimes).map(
137+
([project, time]) =>
138+
project !== "total" && (
139+
<Typography
140+
key={project}
141+
variant="body2"
142+
>
143+
<strong>{project}:</strong> {formatTime(time)} seconds
144+
</Typography>
145+
),
146+
)}
147+
</Box>
148+
</Box>
149+
)}
150+
</DialogContent>
151+
<DialogActions>
152+
<Button onClick={() => setOpenRangeView(false)}>Cancel</Button>
153+
<Button
154+
type="submit"
155+
onClick={handleFetchWorkTime}
156+
>
157+
Get Work Time
158+
</Button>
159+
</DialogActions>
160+
</Dialog>
161+
);
162+
};
163+
164+
export default RangeView;

frontend/src/main.tsx

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,17 @@ const router = createHashRouter([
4646
const projects = await GetAllProjects();
4747
const orgNames = new Map<number, string>();
4848
const orgMap = new Map<number, string>();
49-
useAppStore.getState().organizations.forEach((org) => {
49+
for (const org of useAppStore.getState().organizations) {
5050
orgNames.set(org.id, org.name);
51-
});
51+
}
5252
const projectsMap = new Map<number, string>();
53-
projects.forEach((proj) => {
53+
for (const proj of projects) {
5454
projectsMap.set(proj.id, proj.name);
55-
orgMap.set(proj.id, orgNames.get(proj.organization_id)!);
56-
});
55+
const orgName = orgNames.get(proj.organization_id);
56+
if (orgName) {
57+
orgMap.set(proj.id, orgName);
58+
}
59+
}
5760
return { sessions, projectsMap, orgMap };
5861
},
5962
},
@@ -92,11 +95,14 @@ const timerSubscription = useTimerStore.subscribe(
9295
const openConfirm = useTimerStore.getState().openConfirm;
9396
if (!openConfirm && alertTime > 0) {
9497
console.debug(`Setting confirmation interval for ${alertTime} minutes`);
95-
confirmationInterval = setInterval(() => {
96-
ShowWindow().then(() => {
97-
setOpenConfirm(true);
98-
});
99-
}, 1000 * 60 * alertTime); // Show the alert every x minutes
98+
confirmationInterval = setInterval(
99+
() => {
100+
ShowWindow().then(() => {
101+
setOpenConfirm(true);
102+
});
103+
},
104+
1000 * 60 * alertTime,
105+
); // Show the alert every x minutes
100106
}
101107
} else {
102108
clearInterval(workTimeInterval);
@@ -109,7 +115,7 @@ const timerSubscription = useTimerStore.subscribe(
109115
updateOrgMonthTotal(elapsedTime);
110116
updateProjMonthTotal(elapsedTime);
111117
}
112-
}
118+
},
113119
);
114120
/**
115121
* Update the alert time interval if the user changes it
@@ -121,16 +127,19 @@ const alertTimeSubscription = useAppStore.subscribe(
121127
if (!useTimerStore.getState().running) return;
122128
clearInterval(confirmationInterval);
123129
if (curr > 0) {
124-
confirmationInterval = setInterval(() => {
125-
ShowWindow().then(() => {
126-
setOpenConfirm(true);
127-
});
128-
}, 1000 * 60 * curr); // Show the alert every x minutes
130+
confirmationInterval = setInterval(
131+
() => {
132+
ShowWindow().then(() => {
133+
setOpenConfirm(true);
134+
});
135+
},
136+
1000 * 60 * curr,
137+
); // Show the alert every x minutes
129138
} else {
130139
console.debug("Clearing confirmation interval");
131140
clearInterval(confirmationInterval);
132141
}
133-
}
142+
},
134143
);
135144

136145
const AppWithTheme = () => {
@@ -148,7 +157,7 @@ const AppWithTheme = () => {
148157
}),
149158
},
150159
}),
151-
[appTheme]
160+
[appTheme],
152161
);
153162

154163
return (
@@ -165,10 +174,11 @@ const AppWithTheme = () => {
165174
};
166175

167176
const container = document.getElementById("root");
177+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
168178
const root = createRoot(container!);
169179

170180
root.render(
171181
<React.StrictMode>
172182
<AppWithTheme />
173-
</React.StrictMode>
183+
</React.StrictMode>,
174184
);

0 commit comments

Comments
 (0)