From f2f6f3b8dfc8af983116467c536ba3c37e37d9a0 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Thu, 5 Dec 2024 12:12:07 +0200 Subject: [PATCH 1/9] feat: integrate task label translations, update button logic, and handle February (28/29 days) in calendar --- .../[memberId]/components/CalendarView.tsx | 20 +++++++++------- .../components/FilterWithStatus.tsx | 14 ++++++----- .../components/MonthlyTimesheetCalendar.tsx | 20 ++++++++++++---- .../components/TimeSheetFilterPopover.tsx | 10 ++++---- .../[memberId]/components/TimesheetCard.tsx | 24 +++++++++---------- .../components/WeeklyTimesheetCalendar.tsx | 7 ++++-- apps/web/locales/ar.json | 8 ++++++- apps/web/locales/bg.json | 8 ++++++- apps/web/locales/de.json | 8 ++++++- apps/web/locales/en.json | 8 ++++++- apps/web/locales/es.json | 8 ++++++- apps/web/locales/fr.json | 8 ++++++- apps/web/locales/he.json | 8 ++++++- apps/web/locales/it.json | 8 ++++++- apps/web/locales/nl.json | 8 ++++++- apps/web/locales/pl.json | 8 ++++++- apps/web/locales/pt.json | 8 ++++++- apps/web/locales/ru.json | 8 ++++++- apps/web/locales/zh.json | 8 ++++++- 19 files changed, 147 insertions(+), 52 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx index 360aced88..e1b649a62 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx @@ -12,6 +12,7 @@ import MonthlyTimesheetCalendar from "./MonthlyTimesheetCalendar"; import { useTimelogFilterOptions } from "@/app/hooks"; import WeeklyTimesheetCalendar from "./WeeklyTimesheetCalendar"; interface BaseCalendarDataViewProps { + t: TranslationHooks data: GroupedTimesheet[]; daysLabels?: string[]; CalendarComponent: typeof MonthlyTimesheetCalendar | typeof WeeklyTimesheetCalendar; @@ -35,9 +36,9 @@ export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loa data.length > 0 ? ( <> {timesheetGroupByDays === 'Monthly' ? ( - + ) : timesheetGroupByDays === 'Weekly' ? ( - + ) : ( )} @@ -99,7 +100,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
- + {status === 'DENIED' ? 'REJECTED' : status} ({rows.length}) @@ -166,10 +167,11 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati ) } -const BaseCalendarDataView = ({ data, daysLabels, CalendarComponent }: BaseCalendarDataViewProps) => { +const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCalendarDataViewProps) => { const { getStatusTimesheet } = useTimesheet({}); return (
- + {status === 'DENIED' ? 'REJECTED' : status} - ({rows.length}) + ({rows.length})
@@ -262,10 +264,10 @@ const BaseCalendarDataView = ({ data, daysLabels, CalendarComponent }: BaseCalen ); }; -const MonthlyCalendarDataView = (props: { data: GroupedTimesheet[], daysLabels?: string[] }) => ( +const MonthlyCalendarDataView = (props: { data: GroupedTimesheet[], t: TranslationHooks, daysLabels?: string[] }) => ( ); -const WeeklyCalendarDataView = (props: { data: GroupedTimesheet[], daysLabels?: string[] }) => ( +const WeeklyCalendarDataView = (props: { data: GroupedTimesheet[], t: TranslationHooks, daysLabels?: string[] }) => ( ); diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx index 14ac28f0c..8e4c98a06 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx @@ -3,6 +3,7 @@ import React, { HTMLAttributes } from 'react'; import { Button } from 'lib/components'; import { clsxm } from '@app/utils'; import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { useTranslations } from 'next-intl'; export type FilterStatus = 'All Tasks' | 'Pending' | 'Approved' | 'In review' | 'Draft' | 'Rejected'; export function FilterWithStatus({ @@ -17,6 +18,7 @@ export function FilterWithStatus({ onToggle: (status: FilterStatus) => void; className?: HTMLAttributes; }>) { + const t = useTranslations(); const statusIcons: Record = { 'All Tasks': 'icon-all', @@ -29,12 +31,12 @@ export function FilterWithStatus({ const buttonData = React.useMemo(() => { const counts = { - 'All Tasks': Object.values(data ?? {}).reduce((total, tasks) => total + (tasks?.length ?? 0), 0), - Pending: data?.PENDING?.length ?? 0, - Approved: data?.APPROVED?.length ?? 0, - 'In review': data?.['IN REVIEW']?.length ?? 0, - Draft: data?.DRAFT?.length ?? 0, - Rejected: data?.DENIED?.length ?? 0, + [t('pages.timesheet.ALL_TASKS')]: Object.values(data ?? {}).reduce((total, tasks) => total + (tasks?.length ?? 0), 0), + [t('pages.timesheet.PENDING')]: data?.PENDING?.length ?? 0, + [t('pages.timesheet.APPROVED')]: data?.APPROVED?.length ?? 0, + [t('pages.timesheet.IN_REVIEW')]: data?.['IN REVIEW']?.length ?? 0, + [t('pages.timesheet.DRAFT')]: data?.DRAFT?.length ?? 0, + [t('pages.timesheet.REJECTED')]: data?.DENIED?.length ?? 0, }; return Object.entries(counts).map(([label, count]) => ({ label: label as FilterStatus, diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx index 55f6daad5..c2da23148 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx @@ -1,12 +1,14 @@ import React, { useMemo, useState, useCallback } from "react"; -import { format, addMonths, eachDayOfInterval, startOfMonth, endOfMonth, addDays, Locale } from "date-fns"; +import { format, addMonths, eachDayOfInterval, startOfMonth, endOfMonth, addDays, Locale, isLeapYear } from "date-fns"; import { GroupedTimesheet } from "@/app/hooks/features/useTimesheet"; import { enGB } from 'date-fns/locale'; import { cn } from "@/lib/utils"; import { TotalDurationByDate } from "@/lib/features"; import { formatDate } from "@/app/helpers"; +import { TranslationHooks } from "next-intl"; type MonthlyCalendarDataViewProps = { + t: TranslationHooks data?: GroupedTimesheet[]; onDateClick?: (date: Date) => void; renderDayContent?: (date: Date, plan?: GroupedTimesheet) => React.ReactNode; @@ -26,7 +28,14 @@ const defaultDaysLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const generateFullCalendar = (currentMonth: Date) => { const monthStart = startOfMonth(currentMonth); - const monthEnd = endOfMonth(currentMonth); + const monthEnd = (() => { + const month = monthStart.getMonth(); + if (month === 1) { + const year = monthStart.getFullYear(); + return new Date(year, 1, isLeapYear(monthStart) ? 29 : 28); + } + return endOfMonth(monthStart); + })(); const startDate = addDays(monthStart, -monthStart.getDay()); const endDate = addDays(monthEnd, 6 - monthEnd.getDay()); return eachDayOfInterval({ start: startDate, end: endDate }); @@ -40,7 +49,8 @@ const MonthlyTimesheetCalendar: React.FC = ({ locale = enGB, daysLabels = defaultDaysLabels, noDataText = "No Data", - classNames = {} + classNames = {}, + t }) => { const [currentMonth, setCurrentMonth] = useState(new Date()); const calendarDates = useMemo(() => generateFullCalendar(currentMonth), [currentMonth]); @@ -60,7 +70,7 @@ const MonthlyTimesheetCalendar: React.FC = ({ onClick={handlePreviousMonth} className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 dark:bg-primary-light hover:dark:bg-primary-light" > - Previous + {t('common.PREV')}

{format(currentMonth, "MMMM yyyy", { locale: locale })} @@ -69,7 +79,7 @@ const MonthlyTimesheetCalendar: React.FC = ({ onClick={handleNextMonth} className="px-4 py-2 bg-gray-200 dark:bg-primary-light rounded hover:bg-gray-300 hover:dark:bg-primary-light" > - Next + {t('common.NEXT')}

diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx index 8eb858acc..8cb3c1675 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx @@ -6,9 +6,9 @@ import { MultiSelect } from 'lib/components/custom-select'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import { SettingFilterIcon } from '@/assets/svg'; import { useTranslations } from 'next-intl'; -import { clsxm } from '@/app/utils'; import { useTimelogFilterOptions } from '@/app/hooks'; import { useTimesheet } from '@/app/hooks/features/useTimesheet'; +import { cn } from '@/lib/utils'; export const TimeSheetFilterPopover = React.memo(function TimeSheetFilterPopover() { const [shouldRemoveItems, setShouldRemoveItems] = React.useState(false); @@ -61,7 +61,7 @@ export const TimeSheetFilterPopover = React.memo(function TimeSheetFilterPopover
{ return
0 && status &&
-
+
{status === 'DENIED' ? 'REJECTED' : status} @@ -144,17 +144,17 @@ export const TimesheetCardDetail = ({ data }: { data?: Record
void; renderDayContent?: (date: Date, plan?: GroupedTimesheet) => React.ReactNode; @@ -38,6 +40,7 @@ const WeeklyTimesheetCalendar: React.FC = ({ daysLabels = defaultDaysLabels, noDataText = "No Data", classNames = {}, + t }) => { const [currentDate, setCurrentDate] = useState(new Date()); @@ -61,7 +64,7 @@ const WeeklyTimesheetCalendar: React.FC = ({ onClick={handlePreviousWeek} className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 dark:bg-primary-light hover:dark:bg-primary-light" > - Previous + {t('common.PREV')}

{`Week of ${format(weekDates[0], "MMM d", { locale })} - ${format( @@ -74,7 +77,7 @@ const WeeklyTimesheetCalendar: React.FC = ({ onClick={handleNextWeek} className="px-4 py-2 bg-gray-200 dark:bg-primary-light rounded hover:bg-gray-300 hover:dark:bg-primary-light" > - Next + {t('common.NEXT')}

diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index c94dd66a5..eca9c7ba9 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -633,7 +633,13 @@ }, "LOADING": "جار تحميل بيانات الجدول الزمني...", "NO_ENTRIES_FOUND": "لم يتم العثور على مدخلات في الجدول الزمني", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "أنت على وشك رفض الإدخال المحدد، هل ترغب في المتابعة؟" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "أنت على وشك رفض الإدخال المحدد، هل ترغب في المتابعة؟", + "ALL_TASKS": "جميع المهام", + "PENDING": "قيد الانتظار", + "APPROVED": "تمت الموافقة", + "IN_REVIEW": "قيد المراجعة", + "DRAFT": "مسودة", + "REJECTED": "مرفوض" } }, "timer": { diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 19cc6cc07..0cdd2c793 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -633,7 +633,13 @@ }, "LOADING": "Зареждане на данни за таблицата за работно време...", "NO_ENTRIES_FOUND": "Няма намерени записи в таблицата за работно време", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Вие ще отхвърлите избрания запис, искате ли да продължите?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Вие ще отхвърлите избрания запис, искате ли да продължите?", + "ALL_TASKS": "Всички задачи", + "PENDING": "Висящи", + "APPROVED": "Одобрено", + "IN_REVIEW": "В процес на проверка", + "DRAFT": "Черновик", + "REJECTED": "Отхвърлено" } }, "timer": { diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index fa9e07c55..72fea4a02 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -633,7 +633,13 @@ }, "LOADING": "Lade Zeiterfassungsdaten...", "NO_ENTRIES_FOUND": "Keine Zeiterfassungseinträge gefunden", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Sie sind dabei, den ausgewählten Eintrag abzulehnen. Möchten Sie fortfahren?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Sie sind dabei, den ausgewählten Eintrag abzulehnen. Möchten Sie fortfahren?", + "ALL_TASKS": "Alle Aufgaben", + "PENDING": "Ausstehend", + "APPROVED": "Genehmigt", + "IN_REVIEW": "In Überprüfung", + "DRAFT": "Entwurf", + "REJECTED": "Abgelehnt" } }, "timer": { diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 978ff6969..a01c1cc56 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -633,7 +633,13 @@ }, "LOADING": "Loading timesheet data...", "NO_ENTRIES_FOUND": "No timesheet entries found", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "You are about to reject the selected entry, would you like to proceed?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "You are about to reject the selected entry, would you like to proceed?", + "ALL_TASKS": "All Tasks", + "PENDING": "Pending", + "APPROVED": "Approved", + "IN_REVIEW": "In review", + "DRAFT": "Draft", + "REJECTED": "Rejected" } }, "timer": { diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 5a1b3a1c0..e9b0cadd0 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -633,7 +633,13 @@ }, "LOADING": "Cargando datos del registro de horas...", "NO_ENTRIES_FOUND": "No se encontraron entradas en el registro de horas", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Estás a punto de rechazar la entrada seleccionada, ¿te gustaría continuar?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Estás a punto de rechazar la entrada seleccionada, ¿te gustaría continuar?", + "ALL_TASKS": "Todas las tareas", + "PENDING": "Pendiente", + "APPROVED": "Aprobado", + "IN_REVIEW": "En revisión", + "DRAFT": "Borrador", + "REJECTED": "Rechazado" } }, "timer": { diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 88e51f215..aa682d277 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -633,7 +633,13 @@ }, "LOADING": "Chargement des données de la feuille de temps...", "NO_ENTRIES_FOUND": "Aucune entrée de feuille de temps trouvée", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Vous êtes sur le point de rejeter l'entrée sélectionnée, souhaitez-vous continuer ?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Vous êtes sur le point de rejeter l'entrée sélectionnée, souhaitez-vous continuer ?", + "ALL_TASKS": "Toutes les tâches", + "PENDING": "En attente", + "APPROVED": "Approuvé", + "IN_REVIEW": "En cours d'examen", + "DRAFT": "Brouillon", + "REJECTED": "Rejeté" } }, "timer": { diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 0177769f8..c1305fe5f 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -633,7 +633,13 @@ }, "LOADING": "טוען נתוני רשומת זמן...", "NO_ENTRIES_FOUND": "לא נמצאו רשומות זמן", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "אתה עומד לדחות את הרשומה הנבחרת, האם ברצונך להמשיך?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "אתה עומד לדחות את הרשומה הנבחרת, האם ברצונך להמשיך?", + "ALL_TASKS": "כל המשימות", + "PENDING": "ממתין", + "APPROVED": "מאושר", + "IN_REVIEW": "בבדיקה", + "DRAFT": "טיוטה", + "REJECTED": "נדחה" } }, "timer": { diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index b8e80cd98..02be4b0a7 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -633,7 +633,13 @@ }, "LOADING": "Caricamento dati della registrazione ore...", "NO_ENTRIES_FOUND": "Nessuna voce di registrazione ore trovata", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Stai per rifiutare l'elemento selezionato, vuoi procedere?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Stai per rifiutare l'elemento selezionato, vuoi procedere?", + "ALL_TASKS": "Tutti i compiti", + "PENDING": "In sospeso", + "APPROVED": "Approvato", + "IN_REVIEW": "In revisione", + "DRAFT": "Bozza", + "REJECTED": "Respinto" } }, "timer": { diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 8768d926f..aa792b40a 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -633,7 +633,13 @@ }, "LOADING": "Tijdregistratiegegevens laden...", "NO_ENTRIES_FOUND": "Geen tijdregistratie-invoeren gevonden", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Je staat op het punt de geselecteerde invoer te weigeren, wil je doorgaan?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Je staat op het punt de geselecteerde invoer te weigeren, wil je doorgaan?", + "ALL_TASKS": "Alle taken", + "PENDING": "In behandeling", + "APPROVED": "Goedgekeurd", + "IN_REVIEW": "In beoordeling", + "DRAFT": "Concept", + "REJECTED": "Afgewezen" } }, "timer": { diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 4b98a4d8b..0aac0db99 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -633,7 +633,13 @@ }, "LOADING": "Ładowanie danych rejestru czasu...", "NO_ENTRIES_FOUND": "Nie znaleziono żadnych wpisów w rejestrze czasu", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Zaraz odrzucisz wybraną pozycję, czy chcesz kontynuować?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Zaraz odrzucisz wybraną pozycję, czy chcesz kontynuować?", + "ALL_TASKS": "Wszystkie zadania", + "PENDING": "Oczekujące", + "APPROVED": "Zatwierdzone", + "IN_REVIEW": "W trakcie przeglądu", + "DRAFT": "Szkic", + "REJECTED": "Odrzucone" } }, "timer": { diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index a587193be..4db3c7e9d 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -634,7 +634,13 @@ }, "LOADING": "Загрузка данных учета рабочего времени...", "NO_ENTRIES_FOUND": "Записи учета рабочего времени не найдены", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Você está prestes a rejeitar a entrada selecionada, deseja continuar?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Você está prestes a rejeitar a entrada selecionada, deseja continuar?", + "ALL_TASKS": "Todas as tarefas", + "PENDING": "Pendente", + "APPROVED": "Aprovado", + "IN_REVIEW": "Em revisão", + "DRAFT": "Rascunho", + "REJECTED": "Rejeitado" } }, "timer": { diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 0e7dab1af..65b25b194 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -633,7 +633,13 @@ }, "LOADING": "Загрузка данных учета времени...", "NO_ENTRIES_FOUND": "Записи учета времени не найдены", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Вы собираетесь отклонить выбранную запись, хотите продолжить?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "Вы собираетесь отклонить выбранную запись, хотите продолжить?", + "ALL_TASKS": "Все задачи", + "PENDING": "В ожидании", + "APPROVED": "Утверждено", + "IN_REVIEW": "На рассмотрении", + "DRAFT": "Черновик", + "REJECTED": "Отклонено" } }, "timer": { diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index f53aeb649..19482afe0 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -633,7 +633,13 @@ }, "LOADING": "加载时间表数据...", "NO_ENTRIES_FOUND": "未找到时间表记录", - "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "你即将拒绝选定的条目,是否继续?" + "YOU_ARE_ABOUT_TO_REJECT_ENTRY": "你即将拒绝选定的条目,是否继续?", + "ALL_TASKS": "所有任务", + "PENDING": "待处理", + "APPROVED": "已批准", + "IN_REVIEW": "审核中", + "DRAFT": "草稿", + "REJECTED": "拒绝" } }, "timer": { From 2d92f324730a56a2dd13b434333bb1d085bda9cc Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 5 Dec 2024 22:13:38 +0200 Subject: [PATCH 2/9] fix bug: task detail page crashes --- .../details-section/blocks/task-main-info.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx index 60f2a0d8d..5ad80c544 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx @@ -220,27 +220,23 @@ const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) = const memberInfo = useTeamMemberCard(member); - const unassignedMembers = useMemo( - () => - memberList.filter((member) => - member.employee - ? !task?.members.map((item) => item.userId).includes(member.employee.userId) && - member.employee?.isActive - : false - ), - [memberList, task?.members] - ); + const unassignedMembers = useMemo(() => { + if (!task?.members) return memberList.filter((member) => member.employee?.isActive); // Early return if no task members + const assignedIds = task.members.map((item) => item.userId); - const assignedTaskMembers = useMemo( - () => - memberList.filter((member) => - member.employee - ? task?.members.map((item) => item.userId).includes(member.employee?.userId) && - member.employee?.isActive - : false - ), - [memberList, task?.members] - ); + return memberList.filter( + (member) => member.employee && !assignedIds.includes(member.employee.userId) && member.employee.isActive + ); + }, [memberList, task?.members]); + + const assignedTaskMembers = useMemo(() => { + if (!task?.members) return []; // Early return if no task members + const assignedIds = task.members.map((item) => item.userId); + + return memberList.filter( + (member) => member.employee && assignedIds.includes(member.employee.userId) && member.employee.isActive + ); + }, [memberList, task?.members]); useEffect(() => { if (task && member && memberToRemove) { From 94ec0de4ee850d4b0618d67a016d968198c87467 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Sun, 8 Dec 2024 19:23:51 +0200 Subject: [PATCH 3/9] [Feature] Add time limits report filters (#3392) * feat: add reports filters * fix typo in ITimeReportTableByMemberProps --- .../weekly-limit/components/data-table.tsx | 54 ++++--- .../components/group-by-select.tsx | 133 +++++++++++++----- .../components/time-report-table.tsx | 129 ++++++++++++++--- .../[locale]/reports/weekly-limit/page.tsx | 90 +++++++----- apps/web/app/hooks/features/usePagination.ts | 3 +- apps/web/app/interfaces/ITimeLimits.ts | 12 ++ 6 files changed, 316 insertions(+), 105 deletions(-) diff --git a/apps/web/app/[locale]/reports/weekly-limit/components/data-table.tsx b/apps/web/app/[locale]/reports/weekly-limit/components/data-table.tsx index c83435356..bc2369146 100644 --- a/apps/web/app/[locale]/reports/weekly-limit/components/data-table.tsx +++ b/apps/web/app/[locale]/reports/weekly-limit/components/data-table.tsx @@ -22,7 +22,7 @@ import { formatIntegerToHour, formatTimeString } from '@/app/helpers'; import { ProgressBar } from '@/lib/components'; export type WeeklyLimitTableDataType = { - member: string; + indexValue: string; timeSpent: number; limit: number; percentageUsed: number; @@ -35,12 +35,18 @@ export type WeeklyLimitTableDataType = { * @component * @param {Object} props - The component props. * @param {WeeklyLimitTableDataType[]} props.data - Array of data objects containing weekly time usage information. + * @param {boolean} props.showHeader - If false, hide the header. * * @returns {JSX.Element} A table showing member-wise weekly time limits, usage, and remaining time. * */ -export function DataTableWeeklyLimits(props: { data: WeeklyLimitTableDataType[] }) { +export function DataTableWeeklyLimits(props: { + data: WeeklyLimitTableDataType[]; + indexTitle: string; + showHeader?: boolean; +}) { + const { data, indexTitle, showHeader = true } = props; const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState([]); const [columnVisibility, setColumnVisibility] = React.useState({}); @@ -69,9 +75,9 @@ export function DataTableWeeklyLimits(props: { data: WeeklyLimitTableDataType[] enableHiding: false }, { - accessorKey: 'member', - header: () =>
{t('common.MEMBER')}
, - cell: ({ row }) =>
{row.getValue('member')}
+ accessorKey: 'indexValue', + header: () =>
{indexTitle}
, + cell: ({ row }) =>
{row.getValue('indexValue')}
}, { accessorKey: 'timeSpent', @@ -117,7 +123,7 @@ export function DataTableWeeklyLimits(props: { data: WeeklyLimitTableDataType[] ]; const table = useReactTable({ - data: props.data, + data: data, columns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, @@ -140,21 +146,27 @@ export function DataTableWeeklyLimits(props: { data: WeeklyLimitTableDataType[] {table?.getRowModel()?.rows.length ? (
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - + {showHeader && ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + )} + {table?.getRowModel()?.rows.length ? ( table?.getRowModel().rows.map((row) => ( diff --git a/apps/web/app/[locale]/reports/weekly-limit/components/group-by-select.tsx b/apps/web/app/[locale]/reports/weekly-limit/components/group-by-select.tsx index f9cb705c7..032b830c9 100644 --- a/apps/web/app/[locale]/reports/weekly-limit/components/group-by-select.tsx +++ b/apps/web/app/[locale]/reports/weekly-limit/components/group-by-select.tsx @@ -1,55 +1,120 @@ -import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectItem } from '@components/ui/select'; import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl'; import { useMemo, useState, useCallback } from 'react'; +import { Fragment } from 'react'; +import { Listbox, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; +import { Badge } from '@components/ui/badge'; + +export type TGroupByOption = 'date' | 'week' | 'member'; interface IProps { - onChange: (OPTION: TGroupByOption) => void; - defaultValue: TGroupByOption; + onChange: (options: TGroupByOption[]) => void; + defaultValues: TGroupByOption[]; } /** - * GroupBySelect component provides a dropdown selector for grouping data by day, week, or member. + * [GroupBySelect] - A multi-select component that allows users to choose up to two options + * from a predefined list ("date", "Week", "Member"). + * + * Rules enforced: + * - Only two options can be selected at a time. + * - "date" and "Week" cannot be selected together. + * - At least one option must remain selected. * * @component - * @param {IProps} props - The component props. - * @param {(option: TGroupByOption) => void} props.onChange - Function to handle changes in the selected grouping option. - * @param {TGroupByOption} props.defaultValue - The initial grouping option. + * @param {Object} props - The properties of the component. + * @param {TGroupByOption[]} props.defaultValues - Initial options selected when the component is rendered. + * @param {Function} props.onChange - Callback function invoked when the selection changes. + * + * @returns {JSX.Element} A custom multi-select dropdown with badges representing selected items. * - * @returns {JSX.Element} A dropdown for selecting a grouping option. */ - -export type TGroupByOption = 'Day' | 'Week' | 'Member'; -export function GroupBySelect(props: IProps) { - const { onChange, defaultValue } = props; - const options = useMemo(() => ['Day', 'Week', 'Member'], []); - const [selected, setSelected] = useState(defaultValue); +export function GroupBySelect({ defaultValues, onChange }: IProps) { + const options = useMemo(() => ['date', 'week', 'member'], []); + const [selected, setSelected] = useState(defaultValues); const t = useTranslations(); + const handleChange = useCallback( - (option: TGroupByOption) => { - setSelected(option); - onChange(option); + (options: TGroupByOption[]) => { + // Ensure 'date' and 'Week' cannot be selected together + let updatedOptions = options; + + if (options.includes('date') && options.includes('week')) { + // If 'date' is newly selected, remove 'Week' + if (selected.includes('week')) { + updatedOptions = options.filter((option) => option !== 'week'); + } + // If 'Week' is newly selected, remove 'date' + else if (selected.includes('date')) { + updatedOptions = options.filter((option) => option !== 'date'); + } + } + + setSelected(updatedOptions); + onChange(updatedOptions); }, - [onChange] + [onChange, selected] ); return ( - + + ))} + + + + ); } diff --git a/apps/web/app/[locale]/reports/weekly-limit/components/time-report-table.tsx b/apps/web/app/[locale]/reports/weekly-limit/components/time-report-table.tsx index 7b8349e6c..392b4b32c 100644 --- a/apps/web/app/[locale]/reports/weekly-limit/components/time-report-table.tsx +++ b/apps/web/app/[locale]/reports/weekly-limit/components/time-report-table.tsx @@ -1,12 +1,14 @@ -import { ITimeLimitReport } from '@/app/interfaces/ITimeLimits'; +import { ITimeLimitReport, ITimeLimitReportByEmployee } from '@/app/interfaces/ITimeLimits'; import { DataTableWeeklyLimits } from './data-table'; -import moment from 'moment'; import { DEFAULT_WORK_HOURS_PER_DAY } from '@/app/constants'; +import moment from 'moment'; -interface IProps { +interface ITimeReportTableProps { report: ITimeLimitReport; - displayMode: 'Week' | 'Day'; + displayMode: 'week' | 'date'; organizationLimits: { [key: string]: number }; + indexTitle: string; + header: JSX.Element; } /** @@ -15,24 +17,22 @@ interface IProps { * @component * @param {IProps} props - The component props. * @param {ITimeLimitReport} props.report - Data for employees' time usage reports. - * @param {'Week' | 'Day'} props.displayMode - Specifies whether to display data by week or day. + * @param {'week' | 'date'} props.displayMode - Specifies whether to display data by week or day. * @param {{ [key: string]: number }} props.organizationLimits - Contains organizational limits for time usage, specified by mode. + * @param {JSX.Element} - props.header - The table header * * @returns {JSX.Element} A formatted report table showing time usage and limits. */ -export const TimeReportTable = ({ report, displayMode, organizationLimits }: IProps) => ( +export const TimeReportTable = ({ + report, + displayMode, + organizationLimits, + indexTitle, + header +}: ITimeReportTableProps) => (
-

- {displayMode === 'Week' ? ( - <> - {report.date} - - {moment(report.date).endOf('week').format('YYYY-MM-DD')} - - ) : ( - report.date - )} -

+ {header}
); + +interface ITimeReportTableByMemberProps { + report: ITimeLimitReportByEmployee; + displayMode: 'week' | 'date'; + organizationLimits: { [key: string]: number }; + indexTitle: string; + header: JSX.Element; +} + +/** + * Renders a time report table displaying time tracking data grouped by employee. + * + * @component + * @param {IProps} props - The component props. + * @param {ITimeLimitReportByEmployee} props.report - Data for employees' time usage reports. + * @param {'week' | 'date'} props.displayMode - Specifies whether to display data by week or day. + * @param {{ [key: string]: number }} props.organizationLimits - Contains organizational limits for time usage, specified by mode. + * @param {JSX.Element} - props.header - The table header + * + * @returns {JSX.Element} A formatted report table showing time usage and limits. + */ +export const TimeReportTableByMember = ({ + report, + displayMode, + organizationLimits, + indexTitle, + header +}: ITimeReportTableByMemberProps) => ( +
+
+ {header} +
+
+ { + const limit = item.limit || organizationLimits[displayMode] || DEFAULT_WORK_HOURS_PER_DAY; + const percentageUsed = (item.duration / limit) * 100; + const remaining = limit - item.duration; + + return { + indexValue: + displayMode == 'week' + ? `${item.date} - ${moment(item.date).endOf('week').format('YYYY-MM-DD')}` + : item.date, + limit, + percentageUsed, + timeSpent: item.duration, + remaining + }; + })} + indexTitle={indexTitle} + /> +
+
+); + +/** + * A helper function that groups employee data by employee ID, consolidating their reports across multiple dates. + * + * @param {Array} data - An array of objects representing daily employee reports. + * @param {string} data[].date - The date of the report. + * @param {Array} data[].employees - A list of employees with their work details for the day. + * @returns {ITimeLimitReportByEmployee[]} - An array of grouped employee reports, each containing the employee details and their corresponding reports. + + */ +export function groupDataByEmployee(data: ITimeLimitReport[]): ITimeLimitReportByEmployee[] { + const grouped = new Map(); + + data.forEach((day) => { + const date = day.date; + day.employees.forEach((emp) => { + const empId = emp.employee.id; + + if (!grouped.has(empId)) { + // Initialize new employee entry in the Map + grouped.set(empId, { + employee: emp.employee, + reports: [] + }); + } + + // Add the report for the current date + grouped.get(empId)?.reports.push({ + date: date, + duration: emp.duration, + durationPercentage: emp.durationPercentage, + limit: emp.limit + }); + }); + }); + + // Convert Map values to an array + return Array.from(grouped.values()); +} diff --git a/apps/web/app/[locale]/reports/weekly-limit/page.tsx b/apps/web/app/[locale]/reports/weekly-limit/page.tsx index ad326015e..14917570a 100644 --- a/apps/web/app/[locale]/reports/weekly-limit/page.tsx +++ b/apps/web/app/[locale]/reports/weekly-limit/page.tsx @@ -19,7 +19,7 @@ import { ITimeLimitReport } from '@/app/interfaces/ITimeLimits'; import { getUserOrganizationsRequest } from '@/app/services/server/requests'; import { IOrganization } from '@/app/interfaces'; import { useTranslations } from 'next-intl'; -import { TimeReportTable } from './components/time-report-table'; +import { groupDataByEmployee, TimeReportTable, TimeReportTableByMember } from './components/time-report-table'; function WeeklyLimitReport() { const { isTrackingEnabled } = useOrganizationTeams(); @@ -28,7 +28,7 @@ function WeeklyLimitReport() { const { timeLimitsReports, getTimeLimitsReport } = useTimeLimits(); const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const [groupBy, setGroupBy] = useState('Day'); + const [groupBy, setGroupBy] = useState(['date']); const t = useTranslations(); const breadcrumbPath = useMemo( () => [ @@ -41,8 +41,8 @@ function WeeklyLimitReport() { const organizationLimits = useMemo( () => organization && { - Day: organization.standardWorkHoursPerDay * 3600, - Week: organization.standardWorkHoursPerDay * 3600 * 5 + date: organization.standardWorkHoursPerDay * 3600, + week: organization.standardWorkHoursPerDay * 3600 * 5 }, [organization] ); @@ -54,15 +54,19 @@ function WeeklyLimitReport() { }); const accessToken = useMemo(() => getAccessTokenCookie(), []); const timeZone = useMemo(() => Intl.DateTimeFormat().resolvedOptions().timeZone, []); + const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } = usePagination( - groupBy == 'Week' + groupBy.includes('week') ? timeLimitsReports.filter((report) => moment(report.date).isSame(moment(report.date).startOf('isoWeek'), 'day') ) : timeLimitsReports ); + const duration = useMemo(() => groupBy.find((el) => el == 'date' || el == 'week') ?? 'date', [groupBy]); + const displayMode = (groupBy.find((el) => el === 'date' || el === 'week') ?? 'date') as 'week' | 'date'; + // Get the organization useEffect(() => { if (organizationId && tenantId) { @@ -80,14 +84,14 @@ function WeeklyLimitReport() { employeeIds: [...(member === 'all' ? activeTeam?.members.map((m) => m.employeeId) ?? [] : [member])], startDate: dateRange.from?.toISOString(), endDate: dateRange.to?.toISOString(), - duration: groupBy != 'Member' ? groupBy.toLocaleLowerCase() : 'day', + duration: duration == 'date' ? 'day' : duration, timeZone - //TODO : add groupBy query (when it is ready in the API side) }); }, [ activeTeam?.members, dateRange.from, dateRange.to, + duration, getTimeLimitsReport, groupBy, member, @@ -111,7 +115,7 @@ function WeeklyLimitReport() {

- {groupBy == 'Week' ? t('common.WEEKLY_LIMIT') : t('common.DAILY_LIMIT')} + {groupBy.includes('week') ? t('common.WEEKLY_LIMIT') : t('common.DAILY_LIMIT')}

setMember(memberId)} /> @@ -124,44 +128,66 @@ function WeeklyLimitReport() {
{t('common.GROUP_BY')}: - setGroupBy(option)} /> + setGroupBy(option)} />
} > -
- {organization && - organizationLimits && - currentItems.map((report) => { - const displayMode = groupBy != 'Member' ? groupBy : 'Day'; - - if (displayMode == 'Week') { - if (moment(report.date).isSame(moment(report.date).startOf('isoWeek'), 'day')) { +
+ {organization && organizationLimits ? ( + groupBy.includes('member') ? ( + groupDataByEmployee(timeLimitsReports).map((data) => { + return ( + {data.employee.fullName}} + indexTitle={displayMode} + organizationLimits={organizationLimits} + report={data} + displayMode={displayMode} + key={data.employee.fullName} + /> + ); + }) + ) : ( + currentItems + .filter((report) => + displayMode === 'week' + ? moment(report.date).isSame(moment(report.date).startOf('isoWeek'), 'date') + : true + ) + .map((report) => { return ( + {report.date} - + + {moment(report.date).endOf('week').format('YYYY-MM-DD')} + + + ) : ( +

{report.date}

+ ) + } + indexTitle={t('common.MEMBER')} organizationLimits={organizationLimits} report={report} displayMode={displayMode} key={report.date} /> ); - } else { - return null; - } - } else { - return ( - - ); - } - })} + }) + ) + ) : ( +
{t('common.LOADING')}
+ )}
-
+ { + // TODO : Improve the pagination accordingly to filtered data + } +
(items: T[], defaultItemsPerPage = 10) { itemOffset, endOffset, setItemsPerPage, - currentItems + currentItems, + setItemOffset }; } diff --git a/apps/web/app/interfaces/ITimeLimits.ts b/apps/web/app/interfaces/ITimeLimits.ts index 94df0c0f9..97dbc909e 100644 --- a/apps/web/app/interfaces/ITimeLimits.ts +++ b/apps/web/app/interfaces/ITimeLimits.ts @@ -20,3 +20,15 @@ export interface IGetTimeLimitReport { timeZone?: string; duration?: string; } + +// Grouped time limits data + +export interface ITimeLimitReportByEmployee { + employee: IEmployee; + reports: { + date: string; + duration: number; + durationPercentage: number; + limit: number; + }[]; +} From 3b7409d43ab933a7eff2c7f608193dbdbeafba8d Mon Sep 17 00:00:00 2001 From: Cedric Karungu | Lord VB <83704005+Cedric921@users.noreply.github.com> Date: Sun, 8 Dec 2024 19:24:36 +0200 Subject: [PATCH 4/9] fix: Screenshot Details Card show twice (#3396) * fix: Screenshot Details Card shiw twice * fix: dark theme * fix: coderrabit suggestions --- .../components/screenshoots-per-hour.tsx | 13 +++++++-- .../components/screenshot-details.tsx | 27 ++++++++++--------- .../activity/components/screenshot-item.tsx | 4 +-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/apps/web/lib/features/activity/components/screenshoots-per-hour.tsx b/apps/web/lib/features/activity/components/screenshoots-per-hour.tsx index 1a617c2f8..7220d7c62 100644 --- a/apps/web/lib/features/activity/components/screenshoots-per-hour.tsx +++ b/apps/web/lib/features/activity/components/screenshoots-per-hour.tsx @@ -1,8 +1,11 @@ +"use client" + import { ITimerSlot } from '@app/interfaces/timer/ITimerSlot'; import { clsxm } from '@app/utils'; import ScreenshotDetailsModal from './screenshot-details'; import { useModal } from '@app/hooks'; import ScreenshotItem from './screenshot-item'; +import React, { useCallback } from 'react'; export const ScreenshootPerHour = ({ timeSlots, @@ -14,6 +17,12 @@ export const ScreenshootPerHour = ({ stoppedAt: string; }) => { const { isOpen, closeModal, openModal } = useModal(); + const [selectedElement, setSelectedELement]= React.useState(null) + + const openScreenModal = useCallback((el: ITimerSlot) => { + setSelectedELement(el) + openModal() + }, [openModal]) return (

@@ -27,12 +36,12 @@ export const ScreenshootPerHour = ({ startTime={el.startedAt} percent={el.percentage} imageUrl={el.screenshots[0]?.thumbUrl} - onShow={() => openModal()} + onShow={() => openScreenModal(el)} idSlot={el.id} /> -

))} +
); diff --git a/apps/web/lib/features/activity/components/screenshot-details.tsx b/apps/web/lib/features/activity/components/screenshot-details.tsx index 240890c51..9b9a0014d 100644 --- a/apps/web/lib/features/activity/components/screenshot-details.tsx +++ b/apps/web/lib/features/activity/components/screenshot-details.tsx @@ -4,6 +4,7 @@ import { Modal, ProgressBar, Tooltip } from 'lib/components'; import { ITimerSlot } from '@app/interfaces/timer/ITimerSlot'; import ScreenshotItem from './screenshot-item'; import { useTranslations } from 'next-intl'; +import React from 'react'; const ScreenshotDetailsModal = ({ open, @@ -12,7 +13,7 @@ const ScreenshotDetailsModal = ({ }: { open: boolean; closeModal: () => void; - slot: ITimerSlot; + slot?: ITimerSlot| null; }) => { const t = useTranslations(); return ( @@ -20,18 +21,18 @@ const ScreenshotDetailsModal = ({ isOpen={open} title="Screenshots detail" closeModal={closeModal} - className="bg-white dark:bg-[#343434d4] p-4 rounded-lg lg:w-[60vw] xl:w-[50vw] 2xl:w-[40vw] m-8" + className="bg-white dark:border-[#26272C] dark:bg-[#191a20] dark:border p-4 rounded-lg lg:w-[60vw] xl:w-[50vw] 2xl:w-[40vw]" >

- {new Date(slot.startedAt).toLocaleTimeString()} - {new Date(slot.stoppedAt).toLocaleTimeString()} + {slot ? new Date(slot?.startedAt).toLocaleTimeString() + '-' + new Date(slot?.stoppedAt).toLocaleTimeString(): null}

- +

- {slot.percentage} {t('timer.PERCENT_OF_MINUTES')} + {slot?.percentage} {t('timer.PERCENT_OF_MINUTES')}

- {slot.screenshots.map((screenshot, i) => ( + {slot?.screenshots.map((screenshot, i) => (
{t('timer.KEYBOARD')} - {t('timer.TIMES')} : {slot.keyboard} {slot.keyboardPercentage}% + {t('timer.TIMES')} : {slot?.keyboard} {slot?.keyboardPercentage}%

{t('timer.MOUSE')} - {t('timer.TIMES')} : {slot.mouse} {slot.mousePercentage}% + {t('timer.TIMES')} : {slot?.mouse} {slot?.mousePercentage}%

- {slot.isActive ? ( + {slot?.isActive ? ( {t('timer.ACTIVE')} ) : ( {t('timer.INACTIVE')} )}

- {slot.isArchived ? ( + {slot?.isArchived ? ( {t('timer.ARCHIVED')} ) : ( {t('timer.NOT_ARCHIVED')} @@ -98,4 +99,4 @@ const ScreenshotDetailsModal = ({ ); }; -export default ScreenshotDetailsModal; +export default React.memo(ScreenshotDetailsModal); diff --git a/apps/web/lib/features/activity/components/screenshot-item.tsx b/apps/web/lib/features/activity/components/screenshot-item.tsx index 64657bf62..a64fde709 100644 --- a/apps/web/lib/features/activity/components/screenshot-item.tsx +++ b/apps/web/lib/features/activity/components/screenshot-item.tsx @@ -24,7 +24,7 @@ const ScreenshotItem = ({ return (

{showProgress ? ( From 61170ae68ae96c5adb89db794fdbd89dae15ab94 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Sun, 8 Dec 2024 19:27:33 +0200 Subject: [PATCH 5/9] fix: fix header scroll and card items in the table view - all teams page (#3400) --- apps/web/app/[locale]/all-teams/component.tsx | 84 ++++++++++--------- .../all-teams/users-teams-card/user-card.tsx | 14 ++-- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/apps/web/app/[locale]/all-teams/component.tsx b/apps/web/app/[locale]/all-teams/component.tsx index 184a9a59f..a62530752 100644 --- a/apps/web/app/[locale]/all-teams/component.tsx +++ b/apps/web/app/[locale]/all-teams/component.tsx @@ -5,7 +5,7 @@ import { useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Container } from 'lib/components'; -import { MainHeader, MainLayout } from 'lib/layout'; +import { MainLayout } from 'lib/layout'; import { useOrganizationAndTeamManagers } from '@app/hooks/features/useOrganizationTeamManagers'; import { useEffect } from 'react'; import { useTranslations } from 'next-intl'; @@ -18,55 +18,57 @@ import { MemberFilter } from 'lib/features/all-teams/all-team-members-filter'; import { useOrganizationTeams } from '@app/hooks'; function AllTeamsPage() { - const t = useTranslations(); - const fullWidth = useAtomValue(fullWidthState); - const view = useAtomValue(allTeamsHeaderTabs); - const { filteredTeams, userManagedTeams } = useOrganizationAndTeamManagers(); - const { isTrackingEnabled } = useOrganizationTeams(); + const t = useTranslations(); + const fullWidth = useAtomValue(fullWidthState); + const view = useAtomValue(allTeamsHeaderTabs); + const { filteredTeams, userManagedTeams } = useOrganizationAndTeamManagers(); + const { isTrackingEnabled } = useOrganizationTeams(); - const breadcrumb = [ - { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, - { title: t('common.ALL_TEAMS'), href: '/all-teams' } - ]; + const breadcrumb = [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: t('common.ALL_TEAMS'), href: '/all-teams' } + ]; - /* If the user is not a manager in any team or if he's + /* If the user is not a manager in any team or if he's manager in only one team, then redirect him to the home page */ - if (userManagedTeams.length < 2) return ; + if (userManagedTeams.length < 2) return ; - return ( - - - {/* Breadcrumb */} -
- -
-
- -
- -
-
- -
- - - -
- ); + return ( + +
+ +
+
+ +
+ +
+
+ + {view == IssuesView.CARDS && } +
+ } + > + + + + + ); } function RedirectUser() { - const router = useRouter(); - useEffect(() => { - router.push('/'); - }, [router]); - return <>; + const router = useRouter(); + useEffect(() => { + router.push('/'); + }, [router]); + return <>; } export default withAuthentication(AllTeamsPage, { - displayName: 'AllManagedTeams' + displayName: 'AllManagedTeams' }); diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx index 1920fee16..7e922dc24 100644 --- a/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx +++ b/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx @@ -34,38 +34,38 @@ export default function UserTeamCard({ className )} > -
+
{/* User information */} -
+
{/* Task information */} -
+
- + {/* Task worked Times */} - + {/* Task estimate Info */} - + - + {/* Card Menu */}
From 7cf00f092955dedbb3e191b493e3652b6dc8ae76 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Sun, 8 Dec 2024 19:29:41 +0200 Subject: [PATCH 6/9] [Feat]: Added Bulk Deletion Functionality For Time Entries (#3402) * feat: added bulk deletion functionality for time entries * fix: coderabbitai --- .../[memberId]/components/EditTaskModal.tsx | 2 +- .../hooks/features/useTimelogFilterOptions.ts | 7 +- apps/web/app/hooks/features/useTimesheet.ts | 8 +- .../components/alert-dialog-confirmation.tsx | 72 +++++++++++++++ .../calendar/table-time-sheet.tsx | 92 +++++++++++++------ 5 files changed, 147 insertions(+), 34 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx index 10a66b369..2bb6083bf 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx @@ -131,7 +131,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo isOpen={isOpen} showCloseIcon title={'Edit Task'} - className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:min-w-[32rem] justify-start h-[auto]" + className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[32rem] justify-start h-[auto]" titleClass="font-bold flex justify-start w-full">
diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index a79fcb1ad..bcbe64a23 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -11,6 +11,7 @@ export function useTimelogFilterOptions() { const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState); const [puTimesheetStatus, setPuTimesheetStatus] = useAtom(timesheetUpdateStatus) const [selectedItems, setSelectedItems] = React.useState<{ status: string; date: string }[]>([]); + const [selectTimesheetId, setSelectTimesheetId] = React.useState([]) const employee = employeeState; const project = projectState; @@ -29,7 +30,7 @@ export function useTimelogFilterOptions() { }; const handleSelectRowTimesheet = (items: string) => { - setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items]) + setSelectTimesheetId((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items]) } const handleSelectRowByStatusAndDate = (status: string, date: string) => { @@ -43,7 +44,7 @@ export function useTimelogFilterOptions() { React.useEffect(() => { - return () => setSelectTimesheet([]); + return () => setSelectTimesheetId([]); }, []); return { @@ -56,6 +57,8 @@ export function useTimelogFilterOptions() { setTaskState, setStatusState, handleSelectRowTimesheet, + selectTimesheetId, + setSelectTimesheetId, handleSelectRowByStatusAndDate, selectedItems, selectTimesheet, diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 9add0836d..2f3c6599b 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -95,7 +95,7 @@ export function useTimesheet({ }: TimesheetParams) { const { user } = useAuthenticateUser(); const [timesheet, setTimesheet] = useAtom(timesheetRapportState); - const { employee, project, task, statusState, selectTimesheet: logIds, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions(); + const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions(); const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi); const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi); const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi) @@ -239,7 +239,7 @@ export function useTimesheet({ } - const deleteTaskTimesheet = useCallback(async () => { + const deleteTaskTimesheet = useCallback(async ({ logIds }: { logIds: string[] }) => { if (!user) { throw new Error('User not authenticated'); } @@ -253,14 +253,14 @@ export function useTimesheet({ logIds }); setTimesheet(prevTimesheet => - prevTimesheet.filter(item => !logIds.includes(item.timesheet.id)) + prevTimesheet.filter(item => !logIds.includes(item.id)) ); } catch (error) { console.error('Failed to delete timesheets:', error); throw error; } - }, [user, logIds, queryDeleteTimesheet, setTimesheet]); + }, [user, queryDeleteTimesheet, setTimesheet]); const timesheetElementGroup = useMemo(() => { diff --git a/apps/web/lib/components/alert-dialog-confirmation.tsx b/apps/web/lib/components/alert-dialog-confirmation.tsx index 494a40f12..d16a9afe6 100644 --- a/apps/web/lib/components/alert-dialog-confirmation.tsx +++ b/apps/web/lib/components/alert-dialog-confirmation.tsx @@ -8,6 +8,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@components/ui/alert-dialog" +import { Button, Card, Modal, Text } from 'lib/components'; import { ReloadIcon } from "@radix-ui/react-icons"; import React from "react"; @@ -70,3 +71,74 @@ export function AlertDialogConfirmation({ ); } + + + +export const AlertConfirmationModal = ({ + open, + close, + title, + description, + onAction, + loading, + confirmText = "Continue", + cancelText = "Cancel", + countID = 0 +}: { + open: boolean; + close: () => void; + onAction: () => any; + title: string; + description: string; + loading: boolean; + confirmText?: string; + cancelText?: string; + countID?: number +}) => { + return ( + <> + + +
+
+ + {title} + +
+ + {description} + + {countID > 0 && {countID}} +
+
+
+ + +
+
+
+
+ + ); +}; diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index 117d90d43..db7616a22 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -47,7 +47,7 @@ import { useModal, useTimelogFilterOptions } from '@app/hooks'; import { Checkbox } from '@components/ui/checkbox'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { clsxm } from '@/app/utils'; -import { AlertDialogConfirmation, statusColor } from '@/lib/components'; +import { AlertConfirmationModal, statusColor } from '@/lib/components'; import { Badge } from '@components/ui/badge'; import { EditTaskModal, @@ -64,6 +64,8 @@ import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { toast } from '@components/ui/use-toast'; +import { ToastAction } from '@components/ui/toast'; export const columns: ColumnDef[] = [ { @@ -155,19 +157,22 @@ export const columns: ColumnDef[] = [ ]; export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { - const { isOpen, openModal, closeModal } = useModal(); - + const modal = useModal(); + const alertConfirmationModal = useModal(); + const { isOpen, openModal, closeModal } = modal; + const { isOpen: isOpenAlert, openModal: openAlertConfirmation, closeModal: closeAlertConfirmation } = alertConfirmationModal; const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet, updateTimesheetStatus } = useTimesheet({}); - const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays, handleSelectRowByStatusAndDate } = useTimelogFilterOptions(); + const { timesheetGroupByDays, handleSelectRowByStatusAndDate, handleSelectRowTimesheet, selectTimesheetId, setSelectTimesheetId } = useTimelogFilterOptions(); + + - const [isDialogOpen, setIsDialogOpen] = React.useState(false); const handleConfirm = () => { try { - deleteTaskTimesheet() + deleteTaskTimesheet({ logIds: selectTimesheetId }) .then(() => { - setSelectTimesheet([]); - setIsDialogOpen(false); + setSelectTimesheetId([]); + closeAlertConfirmation() }) .catch((error) => { console.error('Delete timesheet error:', error); @@ -176,9 +181,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { console.error('Delete timesheet error:', error); } }; - const handleCancel = () => { - setIsDialogOpen(false); - }; + const t = useTranslations(); const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState([]); @@ -208,10 +211,10 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { const handleButtonClick = async (action: StatusAction) => { switch (action) { case 'Approved': - if (selectTimesheet.length > 0) { + if (selectTimesheetId.length > 0) { await updateTimesheetStatus({ status: 'APPROVED', - ids: selectTimesheet + ids: selectTimesheetId }) } break; @@ -219,24 +222,24 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { openModal(); break; case 'Deleted': - setIsDialogOpen(true); + openAlertConfirmation(); break; default: console.error(`Unsupported action: ${action}`); } }; + + return (
- { @@ -300,7 +303,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
- {getTimesheetButtons(status as StatusType, t, true, handleButtonClick)} + {getTimesheetButtons(status as StatusType, t, false, handleButtonClick)}
@@ -327,7 +330,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { handleSelectRowTimesheet(task.id)} - checked={selectTimesheet.includes(task.id)} + checked={selectTimesheetId.includes(task.id)} />
{ const { isOpen: isEditTask, openModal: isOpenModalEditTask, closeModal: isCloseModalEditTask } = useModal(); + const { isOpen: isOpenAlert, openModal: openAlertConfirmation, closeModal: closeAlertConfirmation } = useModal(); + const { deleteTaskTimesheet, loadingDeleteTimesheet } = useTimesheet({}); + const t = useTranslations(); + const handleDeleteTask = () => { + deleteTaskTimesheet({ logIds: [dataTimesheet.id] }) + .then(() => { + toast({ + title: 'Deletion Confirmed', + description: "The timesheet has been successfully deleted.", + variant: 'default', + className: 'bg-red-50 text-red-600 border-red-500', + action: Undo + }); + }) + .catch((error) => { + toast({ + title: 'Error during deletion', + description: `An error occurred: ${error}.The timesheet could not be deleted.`, + variant: 'destructive', + className: 'bg-red-50 text-red-600 border-red-500' + }); + }); + }; return ( <> - { + } + /> + {isManage && ( + <> + + + + ) + }
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx index 5c50e0631..83e1f8a91 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx @@ -11,6 +11,8 @@ import { MdKeyboardArrowRight } from 'react-icons/md'; import { PiCalendarDotsThin } from 'react-icons/pi'; import React, { Dispatch, useEffect, useState, SetStateAction, useCallback, useMemo, memo } from 'react'; import moment from 'moment'; +import { ChevronDown } from 'lucide-react'; + interface DatePickerInputProps { date: Date | null; @@ -138,7 +140,7 @@ export function TimesheetFilterDate({
))} diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx index 18606f719..be0640fdf 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx @@ -1,9 +1,10 @@ import { GroupedTimesheet } from '@/app/hooks/features/useTimesheet'; +import { IUser } from '@/app/interfaces'; import TimesheetSkeleton from '@components/shared/skeleton/TimesheetSkeleton'; import { DataTableTimeSheet } from 'lib/features/integrations/calendar'; import { useTranslations } from 'next-intl'; -export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; loading?: boolean }) { +export function TimesheetView({ data, loading, user }: { data?: GroupedTimesheet[]; loading?: boolean, user?: IUser | undefined }) { const t = useTranslations(); if (loading || !data) { @@ -26,7 +27,7 @@ export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; lo return (
- +
); } diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index ade889e9b..a31a9cc60 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -19,9 +19,9 @@ import { CalendarDaysIcon, Clock, User2 } from 'lucide-react'; import { GrTask } from 'react-icons/gr'; import { GoSearch } from 'react-icons/go'; -import { getGreeting } from '@/app/helpers'; +import { getGreeting, secondsToTime } from '@/app/helpers'; import { useTimesheet } from '@/app/hooks/features/useTimesheet'; -import { endOfDay, startOfDay } from 'date-fns'; +import { startOfWeek, endOfWeek } from 'date-fns'; import TimesheetDetailModal from './components/TimesheetDetailModal'; type TimesheetViewMode = 'ListView' | 'CalendarView'; @@ -37,6 +37,7 @@ type ViewToggleButtonProps = { const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memberId: string } }) { const t = useTranslations(); const { user } = useAuthenticateUser(); + const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); const [search, setSearch] = useState(''); const [filterStatus, setFilterStatus] = useLocalStorageState('timesheet-filter-status', 'All Tasks'); const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState( @@ -45,12 +46,12 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb ); const [dateRange, setDateRange] = React.useState<{ from: Date | null; to: Date | null }>({ - from: startOfDay(new Date()), - to: endOfDay(new Date()) + from: startOfWeek(new Date(), { weekStartsOn: 1 }), + to: endOfWeek(new Date(), { weekStartsOn: 1 }), }); - const { timesheet, statusTimesheet, loadingTimesheet } = useTimesheet({ - startDate: dateRange.from ?? '', - endDate: dateRange.to ?? '', + const { timesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({ + startDate: dateRange.from!, + endDate: dateRange.to!, timesheetViewMode: timesheetNavigator }); @@ -88,10 +89,13 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb const username = user?.name || user?.firstName || user?.lastName || user?.username; - + const totalDuration = Object.values(statusTimesheet) + .flat() + .map(entry => entry.timesheet.duration) + .reduce((total, current) => total + current, 0); + const { h: hours, m: minute } = secondsToTime(totalDuration || 0); const fullWidth = useAtomValue(fullWidthState); - const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); const paramsUrl = useParams<{ locale: string }>(); const currentLocale = paramsUrl ? paramsUrl.locale : null; @@ -144,19 +148,23 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb onClick={() => openTimesheetDetail()} /> } classNameIcon="bg-[#3D5A80] shadow-[#3d5a809c] " /> - entry.employee.id) + .filter((id, index, array) => array.indexOf(id) === index) + .length} title="Members Worked" description="People worked since last time" icon={} classNameIcon="bg-[#30B366] shadow-[#30b3678f]" - /> + />)}
@@ -190,6 +198,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
{timesheetNavigator === 'ListView' ? ( ) : ( diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index bcbe64a23..ba00c9aae 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -1,8 +1,10 @@ +import { IUser, RoleNameEnum } from '@/app/interfaces'; import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState, timesheetUpdateStatus } from '@/app/stores'; import { useAtom } from 'jotai'; import React from 'react'; export function useTimelogFilterOptions() { + const [employeeState, setEmployeeState] = useAtom(timesheetFilterEmployeeState); const [projectState, setProjectState] = useAtom(timesheetFilterProjectState); const [statusState, setStatusState] = useAtom(timesheetFilterStatusState); @@ -17,6 +19,15 @@ export function useTimelogFilterOptions() { const project = projectState; const task = taskState + const isUserAllowedToAccess = (user: IUser | null | undefined): boolean => { + const allowedRoles: RoleNameEnum[] = [ + RoleNameEnum.SUPER_ADMIN, + RoleNameEnum.MANAGER, + RoleNameEnum.ADMIN, + ]; + return user?.role.name ? allowedRoles.includes(user.role.name as RoleNameEnum) : false; + }; + const generateTimeOptions = (interval = 15) => { const totalSlots = (24 * 60) / interval; // Total intervals in a day return Array.from({ length: totalSlots }, (_, i) => { @@ -67,6 +78,7 @@ export function useTimelogFilterOptions() { setTimesheetGroupByDays, generateTimeOptions, setPuTimesheetStatus, - puTimesheetStatus + puTimesheetStatus, + isUserAllowedToAccess }; } diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 2f3c6599b..2594b3734 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -95,26 +95,29 @@ export function useTimesheet({ }: TimesheetParams) { const { user } = useAuthenticateUser(); const [timesheet, setTimesheet] = useAtom(timesheetRapportState); - const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions(); + const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess } = useTimelogFilterOptions(); const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi); const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi); const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi) const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi); const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi); - + const isManage = user && isUserAllowedToAccess(user); const getTaskTimesheet = useCallback( ({ startDate, endDate }: TimesheetParams) => { if (!user) return; + const from = moment(startDate).format('YYYY-MM-DD'); - const to = moment(endDate).format('YYYY-MM-DD') + const to = moment(endDate).format('YYYY-MM-DD'); queryTimesheet({ startDate: from, endDate: to, organizationId: user.employee?.organizationId, tenantId: user.tenantId ?? '', timeZone: user.timeZone?.split('(')[0].trim(), - employeeIds: employee?.map((member) => member.employee.id).filter((id) => id !== undefined), + employeeIds: isManage + ? employee?.map(({ employee: { id } }) => id).filter(Boolean) + : [user.employee.id], projectIds: project?.map((project) => project.id).filter((id) => id !== undefined), taskIds: task?.map((task) => task.id).filter((id) => id !== undefined), status: statusState?.map((status) => status.value).filter((value) => value !== undefined) @@ -297,6 +300,7 @@ export function useTimesheet({ loadingCreateTimesheet, updateTimesheet, loadingUpdateTimesheet, - groupByDate + groupByDate, + isManage }; } diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index db7616a22..8d0efecec 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -63,7 +63,7 @@ import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; -import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { IUser, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; import { toast } from '@components/ui/use-toast'; import { ToastAction } from '@components/ui/toast'; @@ -156,15 +156,15 @@ export const columns: ColumnDef[] = [ } ]; -export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { +export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[], user?: IUser | undefined }) { const modal = useModal(); const alertConfirmationModal = useModal(); const { isOpen, openModal, closeModal } = modal; const { isOpen: isOpenAlert, openModal: openAlertConfirmation, closeModal: closeAlertConfirmation } = alertConfirmationModal; const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet, updateTimesheetStatus } = useTimesheet({}); - const { timesheetGroupByDays, handleSelectRowByStatusAndDate, handleSelectRowTimesheet, selectTimesheetId, setSelectTimesheetId } = useTimelogFilterOptions(); - + const { timesheetGroupByDays, handleSelectRowByStatusAndDate, handleSelectRowTimesheet, selectTimesheetId, setSelectTimesheetId, isUserAllowedToAccess } = useTimelogFilterOptions(); + const isManage = isUserAllowedToAccess(user); const handleConfirm = () => { @@ -303,7 +303,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
- {getTimesheetButtons(status as StatusType, t, false, handleButtonClick)} + {isManage && getTimesheetButtons(status as StatusType, t, true, handleButtonClick)}
@@ -336,7 +336,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { - +
))} @@ -469,10 +473,11 @@ export function SelectFilter({ selectedStatus }: { selectedStatus?: string }) { ); } -const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => { +const TaskActionMenu = ({ dataTimesheet, isManage, user }: { dataTimesheet: TimesheetLog, isManage?: boolean, user?: IUser | undefined }) => { const { isOpen: isEditTask, openModal: isOpenModalEditTask, closeModal: isCloseModalEditTask } = useModal(); const { isOpen: isOpenAlert, openModal: openAlertConfirmation, closeModal: closeAlertConfirmation } = useModal(); const { deleteTaskTimesheet, loadingDeleteTimesheet } = useTimesheet({}); + const canEdit = isManage || user?.id === dataTimesheet.employee.user.id; const t = useTranslations(); const handleDeleteTask = () => { @@ -519,9 +524,12 @@ const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => { - - {t('common.EDIT')} - + {canEdit && ( + + {t('common.EDIT')} + + ) + } ; @@ -73,23 +74,32 @@ const formatTime = (hours: number, minutes: number) => (
); -export const DisplayTimeForTimesheet = ({ duration }: { duration: number }) => { +export const DisplayTimeForTimesheet = ({ duration, logType }: { duration: number, logType?: 'TRACKED' | 'MANUAL' | 'IDLE' | undefined }) => { if (duration < 0) { console.warn('Negative duration provided to DisplayTimeForTimesheet'); duration = 0; } const { h: hours, m: minute } = secondsToTime(duration || 0); + + const iconClasses = 'text-[14px] h-4 w-4'; + const icons = { + MANUAL: , + TRACKED: , + IDLE: , + }; + const resolvedLogType: keyof typeof icons = logType ?? 'TRACKED'; return ( -
- -
+
+ {icons[resolvedLogType]} +
{formatTime(hours, minute)}
- ) + ); } + export const TotalTimeDisplay = React.memo(({ timesheetLog }: { timesheetLog: TimesheetLog[] }) => { const totalDuration = Array.isArray(timesheetLog) ? timesheetLog.reduce((acc, curr) => acc + (curr.timesheet?.duration || 0), 0) diff --git a/apps/web/lib/features/team-members-kanban-view.tsx b/apps/web/lib/features/team-members-kanban-view.tsx index b260e0b76..3ab46d943 100644 --- a/apps/web/lib/features/team-members-kanban-view.tsx +++ b/apps/web/lib/features/team-members-kanban-view.tsx @@ -2,7 +2,6 @@ import { useTaskStatus } from '@app/hooks'; import { useKanban } from '@app/hooks/features/useKanban'; import { ITaskStatusItemList, ITeamTask } from '@app/interfaces'; import { IKanban } from '@app/interfaces/IKanban'; -import { fullWidthState } from '@app/stores/fullWidth'; import KanbanDraggable, { EmptyKanbanDroppable } from 'lib/components/Kanban'; import { Fragment, useEffect, useRef, useState } from 'react'; import { @@ -13,7 +12,6 @@ import { DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; -import { useAtomValue } from 'jotai'; import { ScrollArea, ScrollBar } from '@components/ui/scroll-area'; import { cn } from '../utils'; @@ -38,7 +36,6 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: }; }) ); - const fullWidth = useAtomValue(fullWidthState); const containerRef = useRef(null); const { taskStatus: ts } = useTaskStatus(); const reorderTask = (list: ITeamTask[], startIndex: number, endIndex: number) => { @@ -201,7 +198,7 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: return (
@@ -213,46 +210,45 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: className={cn( 'flex flex-1 flex-row gap-2 min-h-fit px-8 lg:px-0 w-full h-full', snapshot.isDraggingOver ? 'bg-slate-200 dark:bg-slate-800' : '', - !fullWidth && 'x-container pl-0' )} ref={provided.innerRef} {...provided.droppableProps} > {columns.length > 0 ? columns.map((column: any, index: number) => ( - - {isColumnCollapse(column.name) ? ( - - ) : ( - - )} - - )) + + {isColumnCollapse(column.name) ? ( + + ) : ( + + )} + + )) : null} {provided.placeholder as React.ReactElement}
diff --git a/apps/web/lib/settings/member-table.tsx b/apps/web/lib/settings/member-table.tsx index 497148d72..f186d14c7 100644 --- a/apps/web/lib/settings/member-table.tsx +++ b/apps/web/lib/settings/member-table.tsx @@ -21,7 +21,7 @@ export const MemberTable = ({ members }: { members: OT_Member[] }) => { const t = useTranslations(); const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } = usePagination(members); - const { activeTeam, updateOrganizationTeam,} = useOrganizationTeams(); + const { activeTeam, updateOrganizationTeam, } = useOrganizationTeams(); const { updateAvatar } = useSettings(); const activeTeamRef = useSyncRef(activeTeam); From 4d939dfec7c99a013fdd0d81318ae32d55b67ecf Mon Sep 17 00:00:00 2001 From: syns Date: Mon, 9 Dec 2024 01:01:19 +0700 Subject: [PATCH 8/9] fix: server web logging running server (#3408) * fix: server web logging running server * style: remove unused code * style: clean code --- apps/server-web/src/main/helpers/constant.ts | 3 +- .../main/helpers/services/libs/server-task.ts | 45 +++++++++----- apps/server-web/src/main/main.ts | 10 +++ apps/server-web/src/renderer/pages/Server.tsx | 61 ++++++++++++++++--- 4 files changed, 95 insertions(+), 24 deletions(-) diff --git a/apps/server-web/src/main/helpers/constant.ts b/apps/server-web/src/main/helpers/constant.ts index bcd77553f..dc547d227 100644 --- a/apps/server-web/src/main/helpers/constant.ts +++ b/apps/server-web/src/main/helpers/constant.ts @@ -50,7 +50,8 @@ export const ServerPageTypeMessage = { export const LOG_TYPES = { UPDATE_LOG: 'UPDATE-LOG', - SERVER_LOG: 'SERVER-LOG' + SERVER_LOG: 'SERVER-LOG', + SERVER_LOG_ERROR: 'SERVER-LOG-ERROR' } export const IPC_TYPES: { diff --git a/apps/server-web/src/main/helpers/services/libs/server-task.ts b/apps/server-web/src/main/helpers/services/libs/server-task.ts index 15cfedc77..e52d8d8e8 100644 --- a/apps/server-web/src/main/helpers/services/libs/server-task.ts +++ b/apps/server-web/src/main/helpers/services/libs/server-task.ts @@ -43,9 +43,8 @@ export abstract class ServerTask { this.loggerObserver = new Observer((msg: string) => { console.log('Sending log_state:', msg); - if (!this.window?.isDestroyed()) { - // this.window.webContents.send('log_state', { msg }); - } + const logType = this.isErrorMessage(msg) ? LOG_TYPES.SERVER_LOG_ERROR : LOG_TYPES.SERVER_LOG; + console.log(logType, msg); }); this.stateObserver = new Observer((state: boolean) => { @@ -64,6 +63,11 @@ export abstract class ServerTask { }); } + private isErrorMessage(msg: string): boolean { + return msg.includes('stderr:') || + this.criticalMessageError.some(error => msg.includes(error)); + } + protected async runTask(signal: AbortSignal): Promise { console.log('Run Server Task'); return new Promise((resolve, reject) => { @@ -72,7 +76,7 @@ export abstract class ServerTask { const service = ChildProcessFactory.createProcess(this.processPath, this.args, signal); - console.log(LOG_TYPES.SERVER_LOG, 'Service created', service.pid); + this.loggerObserver.notify(`Service created ${service.pid}`) service.stdout?.on('data', (data: any) => { const msg = data.toString(); @@ -80,8 +84,12 @@ export abstract class ServerTask { if (msg.includes(this.successMessage)) { const name = String(this.args.serviceName); this.stateObserver.notify(true); + console.log(this.args) this.loggerObserver.notify( - `☣︎ ${name.toUpperCase()} server listen to ${this.config.setting[`${name}Url`]}` + `☣︎ ${name.toUpperCase()} running on http://${this.args.DESKTOP_WEB_SERVER_HOSTNAME}:${this.args.PORT}` + ); + this.loggerObserver.notify( + `☣︎ ${name.toUpperCase()} connected to api ${this.args.GAUZY_API_SERVER_URL}` ); resolve(); } @@ -92,20 +100,12 @@ export abstract class ServerTask { } }); - service.stderr?.on('data', (data: any) => { - console.log(LOG_TYPES.SERVER_LOG, 'stderr:', data.toString()); - this.loggerObserver.notify(data.toString()); - }); + service.stderr?.on('data', this.handleStdErr.bind(this)); - service.on('disconnect', () => { - console.log(LOG_TYPES.SERVER_LOG, 'Webserver disconnected'); - if (this.eventEmitter) { - this.eventEmitter.emit(EventLists.webServerStopped); - } - }) + service.on('disconnect', this.handleDisconnect.bind(this)); service.on('error', (err) => { - console.log('child process error', err); + this.handleError(err, false); }) if (this.eventEmitter) { @@ -168,6 +168,19 @@ export abstract class ServerTask { } } + private handleStdErr(data: any): void { + const errorMessage: string = data.toString(); + this.loggerObserver.notify(`stderr: ${errorMessage}`); + } + + private handleDisconnect(): void { + this.loggerObserver.notify('Webserver disconnected') + if (this.eventEmitter) { + this.eventEmitter.emit(EventLists.webServerStopped); + } + this.stateObserver.notify(false); + } + protected handleError(error: any, attemptKill = true) { if (attemptKill) { this.kill(false); // Pass false to indicate that handleError should not attempt to kill again diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index f633725a7..acc556fde 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -68,6 +68,16 @@ Log.hooks.push((message: any, transport) => { } } + if (message.data[0] === LOG_TYPES.SERVER_LOG_ERROR) { + if (logWindow) { + const msg = message.data.join(' '); + logWindow.webContents.send(IPC_TYPES.SERVER_PAGE, { + type: LOG_TYPES.SERVER_LOG_ERROR, + msg + }); + } + } + if (message.data[0] === LOG_TYPES.UPDATE_LOG) { if (settingWindow) { const msg = `${message.data.join(' ')}`; diff --git a/apps/server-web/src/renderer/pages/Server.tsx b/apps/server-web/src/renderer/pages/Server.tsx index 352d2d185..498324a04 100644 --- a/apps/server-web/src/renderer/pages/Server.tsx +++ b/apps/server-web/src/renderer/pages/Server.tsx @@ -1,25 +1,54 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, ReactNode } from 'react'; import { ServerPageTypeMessage } from '../../main/helpers/constant'; import { IPC_TYPES, LOG_TYPES } from '../../main/helpers/constant'; import { EverTeamsLogo } from '../components/svgs'; import { useTranslation } from 'react-i18next'; +const LogView = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + export function ServerPage() { + const logRef = useRef(null); const [isRun, setIsRun] = useState(false); - const [logs, setLogs] = useState([]); + const [logs, setLogs] = useState< + { + message: string; + type: 'error-log' | 'log'; + }[] + >([]); const [loading, setLoading] = useState(false); const { t } = useTranslation(); + const [logOpen, setLogOpen] = useState(false); useEffect(() => { window.electron.ipcRenderer.removeEventListener(IPC_TYPES.SERVER_PAGE); window.electron.ipcRenderer.on(IPC_TYPES.SERVER_PAGE, (arg: any) => { switch (arg.type) { case LOG_TYPES.SERVER_LOG: - setLogs((prev) => [...prev, arg.msg]); + setLogs((prev) => [ + ...prev, + { + message: arg.msg, + type: 'log', + }, + ]); + scrollToLast(); + break; + case LOG_TYPES.SERVER_LOG_ERROR: + setLogs((prev) => [ + ...prev, + { + message: arg.msg, + type: 'error-log', + }, + ]); + scrollToLast(); break; case ServerPageTypeMessage.SERVER_STATUS: if (arg.data.isRun) { setIsRun(true); + setLogOpen(true); } else { setIsRun(false); } @@ -41,6 +70,12 @@ export function ServerPage() { }); }; + const scrollToLast = () => { + logRef.current?.scrollIntoView({ + behavior: 'smooth', + }); + }; + return (
@@ -60,7 +95,14 @@ export function ServerPage() {
-
+
{ + e.preventDefault(); + setLogOpen((prev) => !prev); + }} + > Server Logs @@ -90,11 +132,16 @@ export function ServerPage() {
{logs.length > 0 && logs.map((log, i) => ( -
- {log} -
+ + {log.type === 'error-log' ? ( + {log.message} + ) : ( + {log.message} + )} + ))}
+
From 34fe05d659b5e83fe36778102e263815c1a303df Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Sun, 8 Dec 2024 20:02:14 +0200 Subject: [PATCH 9/9] [Fix]: Kanban | Cards are broken when Toogle FullWidth Mode (#3410) * fix: Kanban | Cards are broken when toogle FullWidth Mode * fix: cspell