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')}