From a8b20680e5566065022022665dae8defdc3bce21 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 19 May 2025 16:07:35 +0530 Subject: [PATCH 1/9] feat: implement organization working days and hours settings - Added functionality to fetch and update organization working days and hours in the admin center. - Introduced a form for saving working days and hours, with validation and error handling. - Updated the reporting allocation logic to utilize organization-specific working hours for accurate calculations. - Enhanced localization files to support new settings in English, Spanish, and Portuguese. --- .../consolidated-progress-migrations.sql | 2 +- .../src/controllers/reporting-controller.ts | 7 +- .../reporting-allocation-controller.ts | 47 ++++++++---- .../locales/en/admin-center/overview.json | 16 +++- .../locales/es/admin-center/overview.json | 16 +++- .../locales/pt/admin-center/overview.json | 16 +++- .../pages/admin-center/overview/overview.tsx | 74 ++++++++++++++++++- 7 files changed, 157 insertions(+), 21 deletions(-) diff --git a/worklenz-backend/database/migrations/consolidated-progress-migrations.sql b/worklenz-backend/database/migrations/consolidated-progress-migrations.sql index ef89a923..d04c54a8 100644 --- a/worklenz-backend/database/migrations/consolidated-progress-migrations.sql +++ b/worklenz-backend/database/migrations/consolidated-progress-migrations.sql @@ -118,7 +118,7 @@ BEGIN SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id - ), 0) as logged_minutes + ), 0) / 60.0 as logged_minutes FROM tasks t WHERE t.id = _task_id ) diff --git a/worklenz-backend/src/controllers/reporting-controller.ts b/worklenz-backend/src/controllers/reporting-controller.ts index 6825082a..ff0c6a2f 100644 --- a/worklenz-backend/src/controllers/reporting-controller.ts +++ b/worklenz-backend/src/controllers/reporting-controller.ts @@ -415,15 +415,20 @@ export default class ReportingController extends WorklenzControllerBase { @HandleExceptions() public static async getMyTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const selectedTeamId = req.user?.team_id; + if (!selectedTeamId) { + return res.status(400).send(new ServerResponse(false, "No selected team")); + } const q = `SELECT team_id AS id, name FROM team_members tm LEFT JOIN teams ON teams.id = tm.team_id WHERE tm.user_id = $1 + AND tm.team_id = $2 AND role_id IN (SELECT id FROM roles WHERE (admin_role IS TRUE OR owner IS TRUE)) ORDER BY name;`; - const result = await db.query(q, [req.user?.id]); + const result = await db.query(q, [req.user?.id, selectedTeamId]); result.rows.forEach((team: any) => team.selected = true); return res.status(200).send(new ServerResponse(true, result.rows)); } diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 4db8e3d5..98e5b8f5 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -445,27 +445,44 @@ export default class ReportingAllocationController extends ReportingControllerBa } } - // Count only weekdays (Mon-Fri) in the period + // Fetch organization_id from the selected team + const selectedTeamId = req.user?.team_id; + let organizationId: string | undefined = undefined; + if (selectedTeamId) { + const orgIdQuery = `SELECT organization_id FROM teams WHERE id = $1`; + const orgIdResult = await db.query(orgIdQuery, [selectedTeamId]); + organizationId = orgIdResult.rows[0]?.organization_id; + } + + // Fetch organization working hours and working days + let orgWorkingHours = 8; + let orgWorkingDays: { [key: string]: boolean } = { + monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false + }; + if (organizationId) { + const orgHoursQuery = `SELECT working_hours FROM organizations WHERE id = $1`; + const orgHoursResult = await db.query(orgHoursQuery, [organizationId]); + if (orgHoursResult.rows[0]?.working_hours) { + orgWorkingHours = orgHoursResult.rows[0].working_hours; + } + const orgDaysQuery = `SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday FROM organization_working_days WHERE organization_id = $1 ORDER BY created_at DESC LIMIT 1`; + const orgDaysResult = await db.query(orgDaysQuery, [organizationId]); + if (orgDaysResult.rows[0]) { + orgWorkingDays = orgDaysResult.rows[0]; + } + } + + // Count only organization working days in the period let workingDays = 0; let current = startDate.clone(); while (current.isSameOrBefore(endDate, 'day')) { - const day = current.isoWeekday(); - if (day >= 1 && day <= 5) workingDays++; + const weekday = current.format('dddd').toLowerCase(); // e.g., 'monday' + if (orgWorkingDays[weekday]) workingDays++; current.add(1, 'day'); } - // Get hours_per_day for all selected projects - const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`; - const projectHoursResult = await db.query(projectHoursQuery, []); - const projectHoursMap: Record = {}; - for (const row of projectHoursResult.rows) { - projectHoursMap[row.id] = row.hours_per_day || 8; - } - // Sum total working hours for all selected projects - let totalWorkingHours = 0; - for (const pid of Object.keys(projectHoursMap)) { - totalWorkingHours += workingDays * projectHoursMap[pid]; - } + // Use organization working hours for total working hours + const totalWorkingHours = workingDays * orgWorkingHours; const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range); const archivedClause = archived diff --git a/worklenz-frontend/public/locales/en/admin-center/overview.json b/worklenz-frontend/public/locales/en/admin-center/overview.json index efc42855..663c08e5 100644 --- a/worklenz-frontend/public/locales/en/admin-center/overview.json +++ b/worklenz-frontend/public/locales/en/admin-center/overview.json @@ -4,5 +4,19 @@ "owner": "Organization Owner", "admins": "Organization Admins", "contactNumber": "Add Contact Number", - "edit": "Edit" + "edit": "Edit", + "organizationWorkingDaysAndHours": "Organization Working Days & Hours", + "workingDays": "Working Days", + "workingHours": "Working Hours", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "hours": "hours", + "saveButton": "Save", + "saved": "Saved successfully!", + "errorSaving": "Error saving settings." } diff --git a/worklenz-frontend/public/locales/es/admin-center/overview.json b/worklenz-frontend/public/locales/es/admin-center/overview.json index f88dbdf6..c15e15e0 100644 --- a/worklenz-frontend/public/locales/es/admin-center/overview.json +++ b/worklenz-frontend/public/locales/es/admin-center/overview.json @@ -4,5 +4,19 @@ "owner": "Propietario de la Organización", "admins": "Administradores de la Organización", "contactNumber": "Agregar Número de Contacto", - "edit": "Editar" + "edit": "Editar", + "organizationWorkingDaysAndHours": "Días y Horas Laborales de la Organización", + "workingDays": "Días Laborales", + "workingHours": "Horas Laborales", + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo", + "hours": "horas", + "saveButton": "Guardar", + "saved": "¡Guardado exitosamente!", + "errorSaving": "Error al guardar la configuración." } diff --git a/worklenz-frontend/public/locales/pt/admin-center/overview.json b/worklenz-frontend/public/locales/pt/admin-center/overview.json index 7cce8587..6f116067 100644 --- a/worklenz-frontend/public/locales/pt/admin-center/overview.json +++ b/worklenz-frontend/public/locales/pt/admin-center/overview.json @@ -4,5 +4,19 @@ "owner": "Proprietário da Organização", "admins": "Administradores da Organização", "contactNumber": "Adicione o Número de Contato", - "edit": "Editar" + "edit": "Editar", + "organizationWorkingDaysAndHours": "Dias e Horas de Trabalho da Organização", + "workingDays": "Dias de Trabalho", + "workingHours": "Horas de Trabalho", + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo", + "hours": "horas", + "saveButton": "Salvar", + "saved": "Salvo com sucesso!", + "errorSaving": "Erro ao salvar as configurações." } diff --git a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx index e20d103c..9af2137a 100644 --- a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx +++ b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx @@ -1,6 +1,6 @@ import { EditOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons'; import { PageHeader } from '@ant-design/pro-components'; -import { Button, Card, Input, Space, Tooltip, Typography } from 'antd'; +import { Button, Card, Input, Space, Tooltip, Typography, Checkbox, Col, Form, Row, message } from 'antd'; import React, { useEffect, useState } from 'react'; import OrganizationAdminsTable from '@/components/admin-center/overview/organization-admins-table/organization-admins-table'; import { useAppSelector } from '@/hooks/useAppSelector'; @@ -12,6 +12,8 @@ import { adminCenterApiService } from '@/api/admin-center/admin-center.api.servi import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types'; import logger from '@/utils/errorLogger'; import { tr } from 'date-fns/locale'; +import { scheduleAPIService } from '@/api/schedule/schedule.api.service'; +import { Settings } from '@/types/schedule/schedule-v2.types'; const { Text } = Typography; @@ -19,6 +21,10 @@ const Overview: React.FC = () => { const [organization, setOrganization] = useState(null); const [organizationAdmins, setOrganizationAdmins] = useState(null); const [loadingAdmins, setLoadingAdmins] = useState(false); + const [workingDays, setWorkingDays] = useState([]); + const [workingHours, setWorkingHours] = useState(8); + const [saving, setSaving] = useState(false); + const [form] = Form.useForm(); const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode); const { t } = useTranslation('admin-center/overview'); @@ -34,6 +40,19 @@ const Overview: React.FC = () => { } }; + const getOrgWorkingSettings = async () => { + try { + const res = await scheduleAPIService.fetchScheduleSettings(); + if (res && res.done) { + setWorkingDays(res.body.workingDays || ['Monday','Tuesday','Wednesday','Thursday','Friday']); + setWorkingHours(res.body.workingHours || 8); + form.setFieldsValue({ workingDays: res.body.workingDays || ['Monday','Tuesday','Wednesday','Thursday','Friday'], workingHours: res.body.workingHours || 8 }); + } + } catch (error) { + logger.error('Error getting organization working settings', error); + } + }; + const getOrganizationAdmins = async () => { setLoadingAdmins(true); try { @@ -48,8 +67,30 @@ const Overview: React.FC = () => { } }; + const handleSave = async (values: any) => { + setSaving(true); + try { + const res = await scheduleAPIService.updateScheduleSettings({ + workingDays: values.workingDays, + workingHours: values.workingHours, + }); + if (res && res.done) { + message.success(t('saved')); + setWorkingDays(values.workingDays); + setWorkingHours(values.workingHours); + getOrgWorkingSettings(); + } + } catch (error) { + logger.error('Error updating organization working days/hours', error); + message.error(t('errorSaving')); + } finally { + setSaving(false); + } + }; + useEffect(() => { getOrganizationDetails(); + getOrgWorkingSettings(); getOrganizationAdmins(); }, []); @@ -72,6 +113,37 @@ const Overview: React.FC = () => { refetch={getOrganizationDetails} /> + + {t('organizationWorkingDaysAndHours') || 'Organization Working Days & Hours'} +
+ + + + {t('monday')} + {t('tuesday')} + {t('wednesday')} + {t('thursday')} + {t('friday')} + {t('saturday')} + {t('sunday')} + + + + + + + + + +
+
+ {t('admins')} From 1dade05f549bd0a2a42f6c2e1c20bca17eae4c8a Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 07:59:49 +0530 Subject: [PATCH 2/9] feat(reporting): enhance date range handling in reporting allocation - Added support for 'LAST_7_DAYS' and 'LAST_30_DAYS' date ranges in the reporting allocation logic. - Updated date parsing to convert input dates to UTC while preserving the intended local date. - Included console logs for debugging date values during processing. --- .../reporting-allocation-controller.ts | 19 +++++++++++++++++-- worklenz-backend/src/shared/constants.ts | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 98e5b8f5..6b6c1689 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -412,8 +412,14 @@ export default class ReportingAllocationController extends ReportingControllerBa let startDate: moment.Moment; let endDate: moment.Moment; if (date_range && date_range.length === 2) { - startDate = moment(date_range[0]); - endDate = moment(date_range[1]); + // Parse dates and convert to UTC while preserving the intended local date + startDate = moment(date_range[0]).utc().startOf('day'); + endDate = moment(date_range[1]).utc().endOf('day'); + + console.log("Original start date:", date_range[0]); + console.log("Original end date:", date_range[1]); + console.log("UTC startDate:", startDate.format()); + console.log("UTC endDate:", endDate.format()); } else if (duration === DATE_RANGES.ALL_TIME) { // Fetch the earliest start_date (or created_at if null) from selected projects const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; @@ -427,10 +433,18 @@ export default class ReportingAllocationController extends ReportingControllerBa startDate = moment().subtract(1, "day"); endDate = moment().subtract(1, "day"); break; + case DATE_RANGES.LAST_7_DAYS: + startDate = moment().subtract(7, "days"); + endDate = moment(); + break; case DATE_RANGES.LAST_WEEK: startDate = moment().subtract(1, "week").startOf("isoWeek"); endDate = moment().subtract(1, "week").endOf("isoWeek"); break; + case DATE_RANGES.LAST_30_DAYS: + startDate = moment().subtract(30, "days"); + endDate = moment(); + break; case DATE_RANGES.LAST_MONTH: startDate = moment().subtract(1, "month").startOf("month"); endDate = moment().subtract(1, "month").endOf("month"); @@ -480,6 +494,7 @@ export default class ReportingAllocationController extends ReportingControllerBa if (orgWorkingDays[weekday]) workingDays++; current.add(1, 'day'); } + console.log("workingDays", workingDays); // Use organization working hours for total working hours const totalWorkingHours = workingDays * orgWorkingHours; diff --git a/worklenz-backend/src/shared/constants.ts b/worklenz-backend/src/shared/constants.ts index c814c603..f9a9c832 100644 --- a/worklenz-backend/src/shared/constants.ts +++ b/worklenz-backend/src/shared/constants.ts @@ -166,7 +166,9 @@ export const UNMAPPED = "Unmapped"; export const DATE_RANGES = { YESTERDAY: "YESTERDAY", + LAST_7_DAYS: "LAST_7_DAYS", LAST_WEEK: "LAST_WEEK", + LAST_30_DAYS: "LAST_30_DAYS", LAST_MONTH: "LAST_MONTH", LAST_QUARTER: "LAST_QUARTER", ALL_TIME: "ALL_TIME" From 819252cedddbcf9e00ce0133ce9052349af11509 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 08:06:05 +0530 Subject: [PATCH 3/9] refactor(reporting): update date handling and logging in allocation controller - Removed UTC conversion for start and end dates to maintain local date context. - Enhanced console logging to reflect local date values for better debugging. --- .../reporting/reporting-allocation-controller.ts | 10 ++++++---- worklenz-backend/src/cron_jobs/index.ts | 2 +- .../task-drawer/shared/info-tab/task-details-form.tsx | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 6b6c1689..3b949b2b 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -413,13 +413,13 @@ export default class ReportingAllocationController extends ReportingControllerBa let endDate: moment.Moment; if (date_range && date_range.length === 2) { // Parse dates and convert to UTC while preserving the intended local date - startDate = moment(date_range[0]).utc().startOf('day'); - endDate = moment(date_range[1]).utc().endOf('day'); + startDate = moment(date_range[0]).startOf('day'); + endDate = moment(date_range[1]).endOf('day'); console.log("Original start date:", date_range[0]); console.log("Original end date:", date_range[1]); - console.log("UTC startDate:", startDate.format()); - console.log("UTC endDate:", endDate.format()); + console.log("Local startDate:", startDate.format()); + console.log("Local endDate:", endDate.format()); } else if (duration === DATE_RANGES.ALL_TIME) { // Fetch the earliest start_date (or created_at if null) from selected projects const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; @@ -495,6 +495,8 @@ export default class ReportingAllocationController extends ReportingControllerBa current.add(1, 'day'); } console.log("workingDays", workingDays); + console.log("Start date for working days:", startDate.format()); + console.log("End date for working days:", endDate.format()); // Use organization working hours for total working hours const totalWorkingHours = workingDays * orgWorkingHours; diff --git a/worklenz-backend/src/cron_jobs/index.ts b/worklenz-backend/src/cron_jobs/index.ts index 20bd4f62..493b56c8 100644 --- a/worklenz-backend/src/cron_jobs/index.ts +++ b/worklenz-backend/src/cron_jobs/index.ts @@ -7,5 +7,5 @@ export function startCronJobs() { startNotificationsJob(); startDailyDigestJob(); startProjectDigestJob(); - startRecurringTasksJob(); + // startRecurringTasksJob(); } diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx index a2dcaef1..c1c0c935 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx @@ -176,9 +176,9 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => - + {/* - + */} From 3b59a8560b71045baefb0daf84325b66c54f0c28 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 08:08:34 +0530 Subject: [PATCH 4/9] refactor(reporting): simplify date parsing and improve logging format - Updated date parsing to remove UTC conversion, maintaining local date context. - Enhanced console logging to display dates in 'YYYY-MM-DD' format for clarity. - Adjusted date range clause to directly use formatted dates for improved query accuracy. --- .../reporting/reporting-allocation-controller.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 3b949b2b..9f0ae22a 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -412,14 +412,14 @@ export default class ReportingAllocationController extends ReportingControllerBa let startDate: moment.Moment; let endDate: moment.Moment; if (date_range && date_range.length === 2) { - // Parse dates and convert to UTC while preserving the intended local date + // Parse dates without timezone startDate = moment(date_range[0]).startOf('day'); endDate = moment(date_range[1]).endOf('day'); console.log("Original start date:", date_range[0]); console.log("Original end date:", date_range[1]); - console.log("Local startDate:", startDate.format()); - console.log("Local endDate:", endDate.format()); + console.log("Start date:", startDate.format('YYYY-MM-DD')); + console.log("End date:", endDate.format('YYYY-MM-DD')); } else if (duration === DATE_RANGES.ALL_TIME) { // Fetch the earliest start_date (or created_at if null) from selected projects const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; @@ -495,13 +495,13 @@ export default class ReportingAllocationController extends ReportingControllerBa current.add(1, 'day'); } console.log("workingDays", workingDays); - console.log("Start date for working days:", startDate.format()); - console.log("End date for working days:", endDate.format()); + console.log("Start date for working days:", startDate.format('YYYY-MM-DD')); + console.log("End date for working days:", endDate.format('YYYY-MM-DD')); // Use organization working hours for total working hours const totalWorkingHours = workingDays * orgWorkingHours; - const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range); + const durationClause = `AND DATE(task_work_log.created_at) >= '${startDate.format('YYYY-MM-DD')}' AND DATE(task_work_log.created_at) <= '${endDate.format('YYYY-MM-DD')}'`; const archivedClause = archived ? "" : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `; From 14d8f43001440fc29047dd3569240d6bb7897ceb Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 09:23:22 +0530 Subject: [PATCH 5/9] refactor(reporting): clarify date parsing in allocation controller and frontend - Updated comments to specify date parsing format as 'YYYY-MM-DD'. - Modified date range handling in the frontend to format dates using date-fns for consistency. --- .../reporting/reporting-allocation-controller.ts | 2 +- .../members-time-sheet/members-time-sheet.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 9f0ae22a..c8dd5381 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -412,7 +412,7 @@ export default class ReportingAllocationController extends ReportingControllerBa let startDate: moment.Moment; let endDate: moment.Moment; if (date_range && date_range.length === 2) { - // Parse dates without timezone + // Parse simple YYYY-MM-DD dates startDate = moment(date_range[0]).startOf('day'); endDate = moment(date_range[1]).endOf('day'); diff --git a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx index 7283a40a..90ee99de 100644 --- a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx +++ b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx @@ -16,6 +16,7 @@ import { reportingTimesheetApiService } from '@/api/reporting/reporting.timeshee import { IRPTTimeMember } from '@/types/reporting/reporting.types'; import logger from '@/utils/errorLogger'; import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { format } from 'date-fns'; ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels); @@ -141,12 +142,18 @@ const MembersTimeSheet = forwardRef((_, ref) => { const selectedProjects = filterProjects.filter(project => project.selected); const selectedCategories = categories.filter(category => category.selected); + // Format dates using date-fns + const formattedDateRange = dateRange ? [ + format(new Date(dateRange[0]), 'yyyy-MM-dd'), + format(new Date(dateRange[1]), 'yyyy-MM-dd') + ] : undefined; + const body = { teams: selectedTeams.map(t => t.id), projects: selectedProjects.map(project => project.id), categories: selectedCategories.map(category => category.id), duration, - date_range: dateRange, + date_range: formattedDateRange, billable, }; From 67c62fc69b80352adb6de1dbcfbd06333558e95f Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 11:55:29 +0530 Subject: [PATCH 6/9] refactor(schedule): streamline organization working days update query - Simplified the SQL update query for organization working days by removing unnecessary line breaks and improving readability. - Adjusted the subquery to directly select organization IDs, enhancing clarity and maintainability. --- .../src/controllers/schedule-v2/schedule-controller.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts b/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts index a008c06d..a81ec0d0 100644 --- a/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts +++ b/worklenz-backend/src/controllers/schedule-v2/schedule-controller.ts @@ -74,14 +74,9 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase { .map(day => `${day.toLowerCase()} = ${workingDays.includes(day)}`) .join(", "); - const updateQuery = ` - UPDATE public.organization_working_days + const updateQuery = `UPDATE public.organization_working_days SET ${setClause}, updated_at = CURRENT_TIMESTAMP - WHERE organization_id IN ( - SELECT organization_id FROM organizations - WHERE user_id = $1 - ); - `; + WHERE organization_id IN (SELECT id FROM organizations WHERE user_id = $1);`; await db.query(updateQuery, [req.user?.owner_id]); From 76c92b1cc6d969220425884915f7fcf0431a35bc Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 15:51:33 +0530 Subject: [PATCH 7/9] refactor(reporting): optimize date handling and organization working days logic - Simplified date parsing by removing unnecessary start and end of day adjustments. - Streamlined the fetching of organization working days from the database, consolidating queries for improved performance. - Updated the calculation of total working hours to utilize project-specific hours per day, enhancing accuracy in reporting. --- .../reporting-allocation-controller.ts | 98 +++++++++---------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index c8dd5381..17eb2a9e 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -412,14 +412,8 @@ export default class ReportingAllocationController extends ReportingControllerBa let startDate: moment.Moment; let endDate: moment.Moment; if (date_range && date_range.length === 2) { - // Parse simple YYYY-MM-DD dates - startDate = moment(date_range[0]).startOf('day'); - endDate = moment(date_range[1]).endOf('day'); - - console.log("Original start date:", date_range[0]); - console.log("Original end date:", date_range[1]); - console.log("Start date:", startDate.format('YYYY-MM-DD')); - console.log("End date:", endDate.format('YYYY-MM-DD')); + startDate = moment(date_range[0]); + endDate = moment(date_range[1]); } else if (duration === DATE_RANGES.ALL_TIME) { // Fetch the earliest start_date (or created_at if null) from selected projects const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; @@ -433,18 +427,10 @@ export default class ReportingAllocationController extends ReportingControllerBa startDate = moment().subtract(1, "day"); endDate = moment().subtract(1, "day"); break; - case DATE_RANGES.LAST_7_DAYS: - startDate = moment().subtract(7, "days"); - endDate = moment(); - break; case DATE_RANGES.LAST_WEEK: startDate = moment().subtract(1, "week").startOf("isoWeek"); endDate = moment().subtract(1, "week").endOf("isoWeek"); break; - case DATE_RANGES.LAST_30_DAYS: - startDate = moment().subtract(30, "days"); - endDate = moment(); - break; case DATE_RANGES.LAST_MONTH: startDate = moment().subtract(1, "month").startOf("month"); endDate = moment().subtract(1, "month").endOf("month"); @@ -459,49 +445,61 @@ export default class ReportingAllocationController extends ReportingControllerBa } } - // Fetch organization_id from the selected team - const selectedTeamId = req.user?.team_id; - let organizationId: string | undefined = undefined; - if (selectedTeamId) { - const orgIdQuery = `SELECT organization_id FROM teams WHERE id = $1`; - const orgIdResult = await db.query(orgIdQuery, [selectedTeamId]); - organizationId = orgIdResult.rows[0]?.organization_id; - } - - // Fetch organization working hours and working days - let orgWorkingHours = 8; - let orgWorkingDays: { [key: string]: boolean } = { - monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false + // Get organization working days + const orgWorkingDaysQuery = ` + SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday + FROM organization_working_days + WHERE organization_id IN ( + SELECT t.organization_id + FROM teams t + WHERE t.id IN (${teamIds}) + LIMIT 1 + ); + `; + const orgWorkingDaysResult = await db.query(orgWorkingDaysQuery, []); + const workingDaysConfig = orgWorkingDaysResult.rows[0] || { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + sunday: false }; - if (organizationId) { - const orgHoursQuery = `SELECT working_hours FROM organizations WHERE id = $1`; - const orgHoursResult = await db.query(orgHoursQuery, [organizationId]); - if (orgHoursResult.rows[0]?.working_hours) { - orgWorkingHours = orgHoursResult.rows[0].working_hours; - } - const orgDaysQuery = `SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday FROM organization_working_days WHERE organization_id = $1 ORDER BY created_at DESC LIMIT 1`; - const orgDaysResult = await db.query(orgDaysQuery, [organizationId]); - if (orgDaysResult.rows[0]) { - orgWorkingDays = orgDaysResult.rows[0]; - } - } - // Count only organization working days in the period + // Count working days based on organization settings let workingDays = 0; let current = startDate.clone(); while (current.isSameOrBefore(endDate, 'day')) { - const weekday = current.format('dddd').toLowerCase(); // e.g., 'monday' - if (orgWorkingDays[weekday]) workingDays++; + const day = current.isoWeekday(); + if ( + (day === 1 && workingDaysConfig.monday) || + (day === 2 && workingDaysConfig.tuesday) || + (day === 3 && workingDaysConfig.wednesday) || + (day === 4 && workingDaysConfig.thursday) || + (day === 5 && workingDaysConfig.friday) || + (day === 6 && workingDaysConfig.saturday) || + (day === 7 && workingDaysConfig.sunday) + ) { + workingDays++; + } current.add(1, 'day'); } - console.log("workingDays", workingDays); - console.log("Start date for working days:", startDate.format('YYYY-MM-DD')); - console.log("End date for working days:", endDate.format('YYYY-MM-DD')); - // Use organization working hours for total working hours - const totalWorkingHours = workingDays * orgWorkingHours; + // Get hours_per_day for all selected projects + const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`; + const projectHoursResult = await db.query(projectHoursQuery, []); + const projectHoursMap: Record = {}; + for (const row of projectHoursResult.rows) { + projectHoursMap[row.id] = row.hours_per_day || 8; + } + // Sum total working hours for all selected projects + let totalWorkingHours = 0; + for (const pid of Object.keys(projectHoursMap)) { + totalWorkingHours += workingDays * projectHoursMap[pid]; + } - const durationClause = `AND DATE(task_work_log.created_at) >= '${startDate.format('YYYY-MM-DD')}' AND DATE(task_work_log.created_at) <= '${endDate.format('YYYY-MM-DD')}'`; + const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range); const archivedClause = archived ? "" : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `; From 97feef59823a7bab6693d65c0ad039e549a08186 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 16:18:16 +0530 Subject: [PATCH 8/9] refactor(reporting): improve utilization calculations in allocation controller - Updated utilization percentage and utilized hours calculations to handle cases where total working hours are zero, providing 'N/A' for utilization percent when applicable. - Adjusted logic for over/under utilized hours to ensure accurate reporting based on logged time and total working hours. --- .../reporting-allocation-controller.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 17eb2a9e..7d458777 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -522,11 +522,18 @@ export default class ReportingAllocationController extends ReportingControllerBa member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0; member.color_code = getColor(member.name); member.total_working_hours = totalWorkingHours; - member.utilization_percent = (totalWorkingHours > 0 && member.logged_time) ? ((parseFloat(member.logged_time) / (totalWorkingHours * 3600)) * 100).toFixed(2) : '0.00'; - member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00'; - // Over/under utilized hours: utilized_hours - total_working_hours - const overUnder = member.utilized_hours && member.total_working_hours ? (parseFloat(member.utilized_hours) - member.total_working_hours) : 0; - member.over_under_utilized_hours = overUnder.toFixed(2); + if (totalWorkingHours === 0) { + member.utilization_percent = member.logged_time && parseFloat(member.logged_time) > 0 ? 'N/A' : '0.00'; + member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00'; + // Over/under utilized hours: all logged time is over-utilized + member.over_under_utilized_hours = member.utilized_hours; + } else { + member.utilization_percent = (member.logged_time && totalWorkingHours > 0) ? ((parseFloat(member.logged_time) / (totalWorkingHours * 3600)) * 100).toFixed(2) : '0.00'; + member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00'; + // Over/under utilized hours: utilized_hours - total_working_hours + const overUnder = member.utilized_hours && member.total_working_hours ? (parseFloat(member.utilized_hours) - member.total_working_hours) : 0; + member.over_under_utilized_hours = overUnder.toFixed(2); + } } return res.status(200).send(new ServerResponse(true, result.rows)); From c1067d87fe7b5172169b99552578e03c5a787143 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 20 May 2025 16:49:07 +0530 Subject: [PATCH 9/9] refactor(reporting): update total working hours calculation in allocation controller - Replaced project-specific hours per day with organization-wide working hours for total working hours calculation. - Streamlined the SQL query to fetch organization working hours, ensuring accurate reporting based on organizational settings. --- .../reporting-allocation-controller.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 7d458777..ec348952 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -486,18 +486,11 @@ export default class ReportingAllocationController extends ReportingControllerBa current.add(1, 'day'); } - // Get hours_per_day for all selected projects - const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`; - const projectHoursResult = await db.query(projectHoursQuery, []); - const projectHoursMap: Record = {}; - for (const row of projectHoursResult.rows) { - projectHoursMap[row.id] = row.hours_per_day || 8; - } - // Sum total working hours for all selected projects - let totalWorkingHours = 0; - for (const pid of Object.keys(projectHoursMap)) { - totalWorkingHours += workingDays * projectHoursMap[pid]; - } + // Get organization working hours + const orgWorkingHoursQuery = `SELECT working_hours FROM organizations WHERE id = (SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1)`; + const orgWorkingHoursResult = await db.query(orgWorkingHoursQuery, []); + const orgWorkingHours = orgWorkingHoursResult.rows[0]?.working_hours || 8; + let totalWorkingHours = workingDays * orgWorkingHours; const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range); const archivedClause = archived