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 962530f9..b91abfa5 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -445,27 +445,52 @@ export default class ReportingAllocationController extends ReportingControllerBa } } - // Count only weekdays (Mon-Fri) in the period + // 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 + }; + + // Count working days based on organization settings let workingDays = 0; let current = startDate.clone(); while (current.isSameOrBefore(endDate, 'day')) { const day = current.isoWeekday(); - if (day >= 1 && day <= 5) workingDays++; + 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'); } - // 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 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]); 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" 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/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 f01aa3da..9ca546f8 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 @@ -187,9 +187,9 @@ const TaskDetailsForm = ({ taskFormViewModel = null, subTasks = [] }: TaskDetail - + {/* - + */} 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')} 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 e7021893..e51427fa 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); @@ -172,6 +173,13 @@ const MembersTimeSheet = forwardRef( const selectedCategories = categories.filter(category => category.selected); const selectedMembers = members.filter(member => member.selected); const selectedUtilization = utilization.filter(item => item.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), @@ -179,7 +187,7 @@ const MembersTimeSheet = forwardRef( members: selectedMembers.map(member => member.id), utilization: selectedUtilization.map(item => item.id), duration, - date_range: dateRange, + date_range: formattedDateRange, billable, };