From 1a1d0c677377db3c1f3aa57501465ab7fb85c248 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:37:52 +0200 Subject: [PATCH 01/41] [Feat]: Timesheet Filter Calendar (#3305) * refactor: Add FilterCalendar component for improved timesheet selection in calendar view * feat: add clear filter button to reset selected items * fix:coderabbitai[bot] * fix: deepscan-disable-line * fix: logic * fix: logic --- .../components/TimeSheetFilterPopover.tsx | 29 +++-- .../[memberId]/components/TimesheetCard.tsx | 2 +- .../[memberId]/components/TimesheetFilter.tsx | 14 +-- .../components/TimesheetFilterDate.tsx | 107 ++++++++++++++++-- .../[memberId]/components/TimesheetView.tsx | 4 +- .../[locale]/timesheet/[memberId]/page.tsx | 29 +++-- apps/web/app/hooks/features/useTimesheet.ts | 39 ++++--- apps/web/app/interfaces/IProject.ts | 1 + apps/web/app/interfaces/ITask.ts | 2 +- apps/web/app/interfaces/timer/ITimerLog.ts | 3 + .../services/client/api/timer/timer-log.ts | 3 +- .../components/custom-select/multi-select.tsx | 24 ++++ .../calendar/table-time-sheet.tsx | 15 ++- apps/web/lib/features/task/task-issue.tsx | 18 ++- 14 files changed, 229 insertions(+), 61 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx index 364542878..743a0e4ab 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimeSheetFilterPopover.tsx @@ -1,7 +1,8 @@ import { useOrganizationTeams, useTeamTasks } from "@app/hooks"; import { Button } from "@components/ui/button"; import { statusOptions } from "@app/constants"; -import { MultiSelect } from "lib/components/custom-select/multi-select"; +import { MultiSelect } from "lib/components/custom-select"; +import React, { useEffect } from "react"; import { Popover, PopoverContent, @@ -9,13 +10,22 @@ import { } from "@components/ui/popover"; import { SettingFilterIcon } from "@/assets/svg"; import { useTranslations } from "next-intl"; +import { clsxm } from "@/app/utils"; export function TimeSheetFilterPopover() { + const [shouldRemoveItems, setShouldRemoveItems] = React.useState(false); const { activeTeam } = useOrganizationTeams(); const { tasks } = useTeamTasks(); const t = useTranslations(); + + useEffect(() => { + if (shouldRemoveItems) { + setShouldRemoveItems(false); + } + }, [shouldRemoveItems]); + return ( <> @@ -36,9 +46,10 @@ export function TimeSheetFilterPopover() {
(members ? members.employee.fullName : '')} itemId={(item) => item.id} @@ -50,11 +61,12 @@ export function TimeSheetFilterPopover() {
(members ? members.employee.fullName : '')} + removeItems={shouldRemoveItems} + items={activeTeam?.projects ?? []} + itemToString={(project) => (activeTeam?.projects ? project.name! : '')} itemId={(item) => item.id} onValueChange={(selectedItems) => console.log(selectedItems)} multiSelect={true} @@ -64,9 +76,10 @@ export function TimeSheetFilterPopover() {
task} itemId={(task) => (task ? task.id : '')} @@ -78,9 +91,10 @@ export function TimeSheetFilterPopover() {
(status ? status.value : '')} itemId={(item) => item.value} @@ -91,6 +105,7 @@ export function TimeSheetFilterPopover() {
+ +
+
+ + + ); +} diff --git a/apps/web/lib/settings/task-statuses-form.tsx b/apps/web/lib/settings/task-statuses-form.tsx index 99ef5c37f..678e9b569 100644 --- a/apps/web/lib/settings/task-statuses-form.tsx +++ b/apps/web/lib/settings/task-statuses-form.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { useModal, useRefetchData, useTaskStatus } from '@app/hooks'; +import { useModal, useRefetchData, useTaskStatus, useTeamTasks } from '@app/hooks'; import { IIcon, ITaskStatusItemList } from '@app/interfaces'; import { userState } from '@app/stores'; import { clsxm } from '@app/utils'; @@ -15,6 +15,7 @@ import IconPopover from './icon-popover'; import { StatusesListCard } from './list-card'; import SortTasksStatusSettings from '@components/pages/kanban/sort-tasks-status-settings'; import { StandardTaskStatusDropDown } from 'lib/features'; +import { DeleteTaskStatusConfirmationModal } from '../features/task-status/delete-status-confirmation-modal'; type StatusForm = { formOnly?: boolean; @@ -152,6 +153,9 @@ export const TaskStatusesForm = ({ ? updateArray.sort((a: any, b: any) => a.order - b.order) : []; const { isOpen, closeModal, openModal } = useModal(); + const {isOpen : isDeleteConfirmationOpen , closeModal : closeDeleteConfirmationModal, openModal : openDeleteConfirmationModal} = useModal() + const [statusToDelete, setStatusToDelete] = useState(null) + const {tasks} = useTeamTasks() return ( <> @@ -282,8 +286,20 @@ export const TaskStatusesForm = ({ setCreateNew(false); setEdit(status); }} - onDelete={() => { - deleteTaskStatus(status.id); + onDelete={async () => { + try { + const isStatusUsed = tasks.find( + (t) => t.status?.toLowerCase() === status.name?.toLowerCase() + ); + if (isStatusUsed) { + setStatusToDelete(status); + openDeleteConfirmationModal(); + } else { + await deleteTaskStatus(status.id); + } + } catch (error) { + console.error(error); + } }} isStatus={true} /> @@ -298,6 +314,7 @@ export const TaskStatusesForm = ({
+ {statusToDelete && setStatusToDelete(null)} status={statusToDelete} open={isDeleteConfirmationOpen} closeModal={closeDeleteConfirmationModal}/>} ); }; diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index d977cdb04..e05bce974 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "لا يمكن تغيير نوع المهمة الملحمية.", "TASK_HAS_PARENT": "لا يمكن تغيير نوع المهمة حيث أن لديها بالفعل والد." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "أنت على وشك حذف الحالة {statusName} التي يتم استخدامها من قبل المهام النشطة؛ يرجى تأكيد الإجراء" + }, "auth": { "SEND_CODE": "إرسال الرمز", "RESEND_CODE": "إعادة إرسال الرمز", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 8042b15b4..70bebf88c 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Епичният тип задача не може да бъде променен.", "TASK_HAS_PARENT": "Типът задача не може да бъде променен, тъй като задачата вече има родител." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Вие сте на път да изтриете състоянието {statusName}, което се използва от активни задачи; моля, потвърдете действието" + }, "auth": { "SEND_CODE": "изпрати код", "RESEND_CODE": "Изпрати код отново", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index 43c1d47a4..e4938bb7b 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Epische Aufgabentypen können nicht geändert werden.", "TASK_HAS_PARENT": "Der Aufgabentyp kann nicht geändert werden, da die Aufgabe bereits ein übergeordnetes Element hat." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Sie sind dabei, den Status {statusName} zu löschen, der von aktiven Aufgaben verwendet wird; bitte bestätigen Sie die Aktion" + }, "auth": { "SEND_CODE": "Code senden", "RESEND_CODE": "Code erneut senden", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 52974d479..03961d2a3 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Epic Task Type can not be changed.", "TASK_HAS_PARENT": "Task Type can not be changed as Task has already Parent." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "You are about to Delete the Status {statusName} that is used by active tasks; please confirm action" + }, "auth": { "SEND_CODE": "send code", "RESEND_CODE": "Resend Code", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 98ed6351d..ed2e54490 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "La tarea de tipo Épico no se puede cambiar.", "TASK_HAS_PARENT": "El tipo de tarea no se puede cambiar porque la tarea ya tiene un padre." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Está a punto de eliminar el estado {statusName} que está siendo utilizado por tareas activas; por favor confirme la acción" + }, "auth": { "SEND_CODE": "enviar código", "RESEND_CODE": "Reenviar código", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index e0ce01e92..69179edce 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Le type de tâche épique ne peut pas être modifié.", "TASK_HAS_PARENT": "Le type de tâche ne peut pas être modifié car la tâche a déjà un parent." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Vous êtes sur le point de supprimer le statut {statusName} qui est utilisé par des tâches actives ; veuillez confirmer l'action" + }, "auth": { "SEND_CODE": "envoyer le code", "RESEND_CODE": "Renvoyer le code", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 2762188c2..83c9b1312 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "לא ניתן לשנות סוג משימה אפית.", "TASK_HAS_PARENT": "לא ניתן לשנות סוג משימה מכיוון שלמשימה כבר יש הורה." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "אתה עומד למחוק את המצב {statusName} המשמש במשימות פעילות; נא לאשר פעולה" + }, "auth": { "SEND_CODE": "שלח קוד", "RESEND_CODE": "שלח קוד מחדש", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 79e55f0d3..90b440c68 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Il tipo di Compito Epico non può essere cambiato.", "TASK_HAS_PARENT": "Il tipo di Compito non può essere cambiato poiché il Compito ha già un Genitore." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Stai per eliminare lo stato {statusName} utilizzato da attività attive; per favore conferma l'azione" + }, "auth": { "SEND_CODE": "Invia Codice", "RESEND_CODE": "Reinvia Codice", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 8919befc3..90b22103b 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Epic taaktype kan niet worden gewijzigd.", "TASK_HAS_PARENT": "Taaktype kan niet worden gewijzigd omdat de taak al een bovenliggende taak heeft." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "U staat op het punt de status {statusName} te verwijderen die wordt gebruikt door actieve taken; bevestig alstublieft de actie" + }, "auth": { "SEND_CODE": "code verzenden", "RESEND_CODE": "Code opnieuw verzenden", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index fb9a1170a..afabe281f 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Typ Zadania jako 'Epic' nie może zostać zmieniony.", "TASK_HAS_PARENT": "Typ Zadania nie może zostać zmieniony, ponieważ Zadanie ma już przypisanego rodzica." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Zaraz usuniesz status {statusName}, który jest używany przez aktywne zadania; proszę potwierdzić akcję" + }, "auth": { "SEND_CODE": "wyślij kod", "RESEND_CODE": "Wyślij ponownie kod", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index f1a2201b2..63c3f6cf7 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "O Tipo de Tarefa Épica não pode ser alterado.", "TASK_HAS_PARENT": "O Tipo de Tarefa não pode ser alterado, pois a Tarefa já possui um Pai." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Está prestes a eliminar o status {statusName} que está a ser usado por tarefas ativas; por favor, confirme a ação" + }, "auth": { "SEND_CODE": "enviar código", "RESEND_CODE": "Reenviar Código", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index f0afd994e..8f2e0f7e5 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "Тип задачи 'Epic' не может быть изменен.", "TASK_HAS_PARENT": "Тип задачи не может быть изменен, так как у задачи уже есть родитель." }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "Вы собираетесь удалить статус {statusName}, который используется активными задачами; пожалуйста, подтвердите действие" + }, "auth": { "SEND_CODE": "отправить код", "RESEND_CODE": "Отправить код повторно", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 1b63a666f..4484dfded 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -340,6 +340,9 @@ "TASK_IS_ALREADY_EPIC": "史诗类型任务无法更改。", "TASK_HAS_PARENT": "任务已有父任务,无法更改任务类型。" }, + "taskStatus": { + "DELETE_STATUS_CONFIRMATION": "您即将删除正在被活跃任务使用的状态 {statusName};请确认操作" + }, "auth": { "SEND_CODE": "发送验证码", "RESEND_CODE": "重新发送验证码", From 47212e412448ba02c62c56666815c020e69ac606 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Thu, 14 Nov 2024 23:35:52 +0200 Subject: [PATCH 08/41] change position of 'Delete this plan' button (#3322) --- apps/web/lib/features/user-profile-plans.tsx | 141 ++++++++++--------- 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index fae576c33..e9c8a5153 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -58,7 +58,16 @@ export function UserProfilePlans() { const t = useTranslations(); const profile = useUserProfilePage(); - const { todayPlan, futurePlans, pastPlans, outstandingPlans, sortedPlans, profileDailyPlans } = useDailyPlan(); + const { + todayPlan, + futurePlans, + pastPlans, + outstandingPlans, + sortedPlans, + profileDailyPlans, + deleteDailyPlan, + deleteDailyPlanLoading + } = useDailyPlan(); const fullWidth = useAtomValue(fullWidthState); const [currentOutstanding, setCurrentOutstanding] = useLocalStorageState('outstanding', 'ALL'); const [currentTab, setCurrentTab] = useLocalStorageState('daily-plan-tab', 'Today Tasks'); @@ -85,6 +94,8 @@ export function UserProfilePlans() { const { hasPlan } = useTimer(); const { activeTeam } = useTeamTasks(); const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); + const [popupOpen, setPopupOpen] = useState(false); + const canSeeActivity = useCanSeeActivityScreen(); // Set the tab plan tab to outstanding if user has no daily plan and there are outstanding tasks (on first load) useEffect(() => { @@ -132,7 +143,7 @@ export function UserProfilePlans() { {profileDailyPlans?.items?.length > 0 ? (
-
+
{Object.keys(tabsScreens).map((filter, i) => (
{i !== 0 && } @@ -173,6 +184,66 @@ export function UserProfilePlans() { ))}
+ {currentTab === 'Today Tasks' && todayPlan[0] && ( + <> + {canSeeActivity ? ( +
+ { + setPopupOpen((prev) => !prev); + }} + variant="outline" + className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" + > + {t('common.plan.DELETE_THIS_PLAN')} + + } + > + {/*button confirm*/} + + {/*button cancel*/} + + +
+ ) : ( + <> + )} + + )} {currentTab === 'Outstanding' && ( table.getColumn('title')?.setFilterValue(event.target.value)} - className="max-w-sm pl-10 bg-transparent border-none placeholder:font-normal" + className="max-w-sm pl-10 bg-transparent border-none dark:focus-visible:!border-[#c8c8c8] transition-all duration-200 placeholder:font-normal" />
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx index db5d266a8..9d5888f63 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx @@ -175,7 +175,7 @@ export function TimesheetFilterDate({
- - {/*
- - { - activeTeam ? - setTeam(team)} - itemId={(team) => (team ? team.id : '')} - itemToString={(team) => (team ? team.name : '')} - triggerClassName="border-gray-300 dark:border-slate-600" - /> - : - <> - } -
*/} - {params === 'AddManuelTime' ? ( <> Date: Thu, 14 Nov 2024 23:47:34 +0200 Subject: [PATCH 13/41] improve arrow navigation on 'see plans' modal (#3337) * improve arrow navigation on 'see plans' modal * add requested changes --- .../features/daily-plan/all-plans-modal.tsx | 95 ++++++++++++++++--- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/apps/web/lib/features/daily-plan/all-plans-modal.tsx b/apps/web/lib/features/daily-plan/all-plans-modal.tsx index 63bebcaef..0fb7db8cd 100644 --- a/apps/web/lib/features/daily-plan/all-plans-modal.tsx +++ b/apps/web/lib/features/daily-plan/all-plans-modal.tsx @@ -19,7 +19,8 @@ interface IAllPlansModal { isOpen: boolean; } -type CalendarTab = 'Today' | 'Tomorrow' | 'Calendar'; +type TCalendarTab = 'Today' | 'Tomorrow' | 'Calendar'; +type TNavigationMode = 'DATE' | 'PLAN'; /** * A modal that displays all the plans available to the user (Today, Tomorrow and Future). @@ -32,17 +33,41 @@ type CalendarTab = 'Today' | 'Tomorrow' | 'Calendar'; * @returns {JSX.Element} The modal element */ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) { + // Utility function for checking if two dates are the same + const isSameDate = useCallback((date1: Date | number | string, date2: Date | number | string) => { + return moment(date1).toISOString().split('T')[0] === moment(date2).toISOString().split('T')[0]; + }, []); + const { isOpen, closeModal } = props; const [showCalendar, setShowCalendar] = useState(false); const [showCustomPlan, setShowCustomPlan] = useState(false); const [customDate, setCustomDate] = useState(moment().toDate()); const { myDailyPlans, pastPlans } = useDailyPlan(); const t = useTranslations(); - - // Utility function for checking if two dates are the same - const isSameDate = useCallback((date1: Date | number | string, date2: Date | number | string) => { - return moment(date1).toISOString().split('T')[0] === moment(date2).toISOString().split('T')[0]; - }, []); + const [navigationMode, setNavigationMode] = useState('PLAN'); + const sortedPlans = useMemo( + () => + [...myDailyPlans.items].sort((plan1, plan2) => + new Date(plan1.date).getTime() > new Date(plan2.date).getTime() ? 1 : -1 + ), + [myDailyPlans.items] + ); + const currentPlanIndex = useMemo( + () => sortedPlans.findIndex((plan) => isSameDate(plan.date, moment(customDate).toDate())), + // eslint-disable-next-line react-hooks/exhaustive-deps + [customDate, myDailyPlans.items] + ); + const nextPlan = useMemo( + () => + currentPlanIndex >= 0 && currentPlanIndex < myDailyPlans.items.length - 1 + ? sortedPlans[currentPlanIndex + 1] + : null, + [currentPlanIndex, myDailyPlans.items.length, sortedPlans] + ); + const previousPlan = useMemo( + () => (currentPlanIndex > 0 ? sortedPlans[currentPlanIndex - 1] : null), + [currentPlanIndex, sortedPlans] + ); // Memoize today, tomorrow, and future plans const todayPlan = useMemo( @@ -70,21 +95,27 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) }, [closeModal]); // Define tabs for plan selection - const tabs: CalendarTab[] = useMemo(() => ['Today', 'Tomorrow', 'Calendar'], []); + const tabs: TCalendarTab[] = useMemo(() => ['Today', 'Tomorrow', 'Calendar'], []); // State to track the active tab const [selectedTab, setSelectedTab] = useState(tabs[0]); // Handle tab switching - const handleTabClick = (tab: CalendarTab) => { + const handleTabClick = (tab: TCalendarTab) => { if (tab === 'Today') { setCustomDate(moment().toDate()); + setNavigationMode('PLAN'); } else if (tab === 'Tomorrow') { setCustomDate(moment().add(1, 'days').toDate()); + setNavigationMode('PLAN'); } setSelectedTab(tab); setShowCalendar(tab === 'Calendar'); setShowCustomPlan(false); + + if (tab === 'Calendar') { + setNavigationMode('DATE'); + } }; // Determine which plan to display based on the selected tab @@ -117,6 +148,8 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) setShowCalendar(false); setShowCustomPlan(true); } + + setNavigationMode('PLAN'); } }, [customDate, isSameDate]); @@ -155,12 +188,12 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) if (selectedPlan) { if (isSameDate(date, moment().startOf('day').toDate())) { - setSelectedTab('Today'); + navigationMode == 'PLAN' && setSelectedTab('Today'); } else if (isSameDate(date, moment().add(1, 'days').startOf('day').toDate())) { - setSelectedTab('Tomorrow'); + navigationMode == 'PLAN' && setSelectedTab('Tomorrow'); } else { setSelectedTab('Calendar'); - if (existPlan) { + if (existPlan && navigationMode == 'PLAN') { setShowCalendar(false); setShowCustomPlan(true); } else { @@ -171,11 +204,35 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) } } }, - [isSameDate, myDailyPlans.items, selectedPlan] + [isSameDate, myDailyPlans.items, navigationMode, selectedPlan] + ); + + // Handle navigation between plans + const moveBetweenPlans = useCallback( + (direction: boolean) => { + if (direction) { + // Select the next plan + if (nextPlan) { + setCustomDate(moment(nextPlan.date).toDate()); + setSelectedTab('Calendar'); + setShowCalendar(false); + setShowCustomPlan(true); + } + } else { + // Select the previous plan + if (previousPlan) { + setCustomDate(moment(previousPlan.date).toDate()); + setSelectedTab('Calendar'); + setShowCalendar(false); + setShowCustomPlan(true); + } + } + }, + [nextPlan, previousPlan] ); // A handler function to display the plan title - const displayPlanTitle = (selectedTab: CalendarTab, selectedPlan?: IDailyPlan) => { + const displayPlanTitle = (selectedTab: TCalendarTab, selectedPlan?: IDailyPlan) => { const isCalendarTab = selectedTab === 'Calendar'; const planDate = selectedPlan?.date ? new Date(selectedPlan.date).toLocaleDateString('en-GB') : ''; const hasTasks = selectedPlan?.tasks?.length; @@ -247,14 +304,22 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal)
arrowNavigationHandler(moment(customDate).subtract(1, 'days').toDate())} + onClick={() => + navigationMode === 'DATE' + ? arrowNavigationHandler(moment(customDate).subtract(1, 'days').toDate()) + : moveBetweenPlans(false) + } className="rotate-180 cursor-pointer px-2 h-full flex items-center justify-center" > arrowNavigationHandler(moment(customDate).add(1, 'days').toDate())} + onClick={() => + navigationMode === 'DATE' + ? arrowNavigationHandler(moment(customDate).add(1, 'days').toDate()) + : moveBetweenPlans(true) + } className=" h-full cursor-pointer flex px-2 items-center justify-center" > From 5c1cada2d05ceaf17f3b3ae46a765f88f7bcf6f5 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Sun, 17 Nov 2024 10:01:22 +0200 Subject: [PATCH 14/41] [Feat]: Add Delete Task Timesheet function (#3338) * Add deleteTaskTimesheet function to handle timesheet deletion * fix: coderabbitai * fix: build * feat: make AlertDialogConfirmation fully controllable via external props * fix: coderabbitai * fix: coderabbitai --- .../[memberId]/components/TimesheetAction.tsx | 12 +- .../api/timer/timesheet/bulk-delete/route.ts | 42 ++++++ .../hooks/features/useTimelogFilterOptions.ts | 15 +- apps/web/app/hooks/features/useTimesheet.ts | 47 +++++- .../services/client/api/timer/timer-log.ts | 41 +++++- .../app/services/server/requests/timesheet.ts | 16 ++ apps/web/app/stores/time-logs.ts | 1 + apps/web/components/ui/alert-dialog.tsx | 139 ++++++++++++++++++ apps/web/components/ui/button.tsx | 89 +++++------ .../components/alert-dialog-confirmation.tsx | 72 +++++++++ apps/web/lib/components/index.ts | 1 + .../calendar/table-time-sheet.tsx | 50 ++++++- apps/web/lib/features/user-profile-plans.tsx | 5 +- apps/web/package.json | 9 +- yarn.lock | 14 +- 15 files changed, 484 insertions(+), 69 deletions(-) create mode 100644 apps/web/app/api/timer/timesheet/bulk-delete/route.ts create mode 100755 apps/web/components/ui/alert-dialog.tsx create mode 100644 apps/web/lib/components/alert-dialog-confirmation.tsx diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx index 2bad95b8a..213b9ccc0 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx @@ -24,23 +24,25 @@ export const TimesheetButton = ({ className, icon, onClick, title }: ITimesheetB export type StatusType = "Pending" | "Approved" | "Rejected"; +export type StatusAction = "Deleted" | "Approved" | "Rejected"; + // eslint-disable-next-line @typescript-eslint/no-empty-function -export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, onClick: (action: StatusType) => void) => { +export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, onClick: (action: StatusAction) => void) => { - const buttonsConfig: Record = { + const buttonsConfig: Record = { Pending: [ { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" }, { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" }, - { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" } + { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" } ], Approved: [ { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" }, - { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" } + { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" } ], Rejected: [ { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" }, - { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" } + { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" } ] }; diff --git a/apps/web/app/api/timer/timesheet/bulk-delete/route.ts b/apps/web/app/api/timer/timesheet/bulk-delete/route.ts new file mode 100644 index 000000000..517e1f624 --- /dev/null +++ b/apps/web/app/api/timer/timesheet/bulk-delete/route.ts @@ -0,0 +1,42 @@ +import { deleteTaskTimesheetRequest } from '@/app/services/server/requests'; +import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app'; +import { NextResponse } from "next/server"; + +export async function DELETE(req: Request) { + const res = new NextResponse(); + const body = await req.json(); + const { logIds = [] } = body; + + if (!Array.isArray(logIds) || logIds.length === 0) { + return NextResponse.json( + { error: 'logIds must be a non-empty array' }, + { status: 400 } + ); + } + + const { $res, user, tenantId, organizationId, access_token, } = await authenticatedGuard(req, res); + if (!user) return $res('Unauthorized'); + try { + const { data } = await deleteTaskTimesheetRequest({ + tenantId, + organizationId, + logIds, + }, access_token); + + if (!data) { + return NextResponse.json( + { error: 'No data found' }, + { status: 404 } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error delete timesheet:', error); + return NextResponse.json( + { error: 'Failed to delete timesheet data' }, + { status: 500 } + ); + } + +} diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index 60733481f..ca39217a3 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -1,16 +1,24 @@ -import { timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores'; +import { timesheetDeleteState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } 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); const [taskState, setTaskState] = useAtom(timesheetFilterTaskState); + const [selectTimesheet, setSelectTimesheet] = useAtom(timesheetDeleteState); const employee = employeeState; const project = projectState; const task = taskState + const handleSelectRowTimesheet = (items: string) => { + setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items]) + } + React.useEffect(() => { + return () => setSelectTimesheet([]); + }, []); return { statusState, @@ -20,6 +28,9 @@ export function useTimelogFilterOptions() { setEmployeeState, setProjectState, setTaskState, - setStatusState + setStatusState, + handleSelectRowTimesheet, + selectTimesheet, + setSelectTimesheet }; } diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index c11e196ff..959f2ccdd 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -3,7 +3,7 @@ import { useAtom } from 'jotai'; import { timesheetRapportState } from '@/app/stores/time-logs'; import { useQuery } from '../useQuery'; import { useCallback, useEffect } from 'react'; -import { getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log'; +import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log'; import moment from 'moment'; import { ITimeSheet } from '@/app/interfaces'; import { useTimelogFilterOptions } from './useTimelogFilterOptions'; @@ -17,7 +17,11 @@ export interface GroupedTimesheet { date: string; tasks: ITimeSheet[]; } - +interface DeleteTimesheetParams { + organizationId: string; + tenantId: string; + logIds: string[]; +} const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => { if (!items?.length) return []; @@ -46,8 +50,10 @@ export function useTimesheet({ }: TimesheetParams) { const { user } = useAuthenticateUser(); const [timesheet, setTimesheet] = useAtom(timesheetRapportState); - const { employee, project } = useTimelogFilterOptions(); + const { employee, project, selectTimesheet: logIds } = useTimelogFilterOptions(); const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi); + const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi) + const getTaskTimesheet = useCallback( ({ startDate, endDate }: TimesheetParams) => { @@ -76,6 +82,39 @@ export function useTimesheet({ project ] ); + + + + const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => { + try { + return await queryDeleteTimesheet(params); + } catch (error) { + console.error('Error deleting timesheet:', error); + throw error; + } + }; + + const deleteTaskTimesheet = useCallback(async () => { + if (!user) { + throw new Error('User not authenticated'); + } + if (!logIds.length) { + throw new Error('No timesheet IDs provided for deletion'); + } + try { + await handleDeleteTimesheet({ + organizationId: user.employee.organizationId, + tenantId: user.tenantId ?? "", + logIds + }); + } catch (error) { + console.error('Failed to delete timesheets:', error); + throw error; + } + }, + [user, queryDeleteTimesheet, logIds, handleDeleteTimesheet] // deepscan-disable-line + ); + useEffect(() => { getTaskTimesheet({ startDate, endDate }); }, [getTaskTimesheet, startDate, endDate]); @@ -86,5 +125,7 @@ export function useTimesheet({ loadingTimesheet, timesheet: groupByDate(timesheet), getTaskTimesheet, + loadingDeleteTimesheet, + deleteTaskTimesheet }; } diff --git a/apps/web/app/services/client/api/timer/timer-log.ts b/apps/web/app/services/client/api/timer/timer-log.ts index f3a5b13b4..3715f8cc3 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -1,5 +1,5 @@ import { ITimeSheet, ITimerStatus } from '@app/interfaces'; -import { get } from '../../axios'; +import { get, deleteApi } from '../../axios'; export async function getTimerLogs( tenantId: string, @@ -66,3 +66,42 @@ export async function getTaskTimesheetLogsApi({ const endpoint = `/timesheet/time-log?${params.toString()}`; return get(endpoint, { tenantId }); } + + +export async function deleteTaskTimesheetLogsApi({ + logIds, + organizationId, + tenantId +}: { + organizationId: string, + tenantId: string, + logIds: string[] +}) { + // Validate required parameters + if (!organizationId || !tenantId || !logIds?.length) { + throw new Error('Required parameters missing: organizationId, tenantId, and logIds are required'); + } + + // Limit bulk deletion size for safety + if (logIds.length > 100) { + throw new Error('Maximum 100 logs can be deleted at once'); + } + + const params = new URLSearchParams({ + organizationId, + tenantId + }); + logIds.forEach((id, index) => { + if (!id) { + throw new Error(`Invalid logId at index ${index}`); + } + params.append(`logIds[${index}]`, id); + }); + + const endPoint = `/timesheet/time-log?${params.toString()}`; + try { + return await deleteApi<{ success: boolean; message: string }>(endPoint, { tenantId }); + } catch (error) { + throw new Error(`Failed to delete timesheet logs`); + } +} diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index a7a254880..1c5fe6048 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -79,3 +79,19 @@ export function getTaskTimesheetRequest(params: ITimesheetProps, bearer_token: s tenantId: params.tenantId }) } + +type IDeleteTimesheetProps = { + organizationId: string; + tenantId: string; + logIds?: string[] +} + +export function deleteTaskTimesheetRequest(params: IDeleteTimesheetProps, bearer_token: string) { + const { logIds = [] } = params; + return serverFetch({ + path: `/timesheet/time-log/${logIds.join(',')}`, + method: 'DELETE', + bearer_token, + tenantId: params.tenantId + }); +} diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index 9f607b4ff..e73d66c64 100644 --- a/apps/web/app/stores/time-logs.ts +++ b/apps/web/app/stores/time-logs.ts @@ -16,3 +16,4 @@ export const timesheetFilterProjectState = atom([]); export const timesheetFilterTaskState = atom([]); export const timesheetFilterStatusState = atom([]); +export const timesheetDeleteState = atom([]) diff --git a/apps/web/components/ui/alert-dialog.tsx b/apps/web/components/ui/alert-dialog.tsx new file mode 100755 index 000000000..61b539897 --- /dev/null +++ b/apps/web/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "lib/utils" +import { buttonVariants } from "components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx index 90859cca5..670456047 100644 --- a/apps/web/components/ui/button.tsx +++ b/apps/web/components/ui/button.tsx @@ -1,51 +1,56 @@ -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; -import * as React from 'react'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from 'lib/utils'; +import { cn } from "lib/utils" const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline' - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10' - } - }, - defaultVariants: { - variant: 'default', - size: 'default' - } - } -); + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }: ButtonProps, ref) => { - const Comp = asChild ? Slot : 'button'; - return ( - - {props.children as React.ReactNode} - - ); - } -); -Button.displayName = 'Button'; + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/apps/web/lib/components/alert-dialog-confirmation.tsx b/apps/web/lib/components/alert-dialog-confirmation.tsx new file mode 100644 index 000000000..494a40f12 --- /dev/null +++ b/apps/web/lib/components/alert-dialog-confirmation.tsx @@ -0,0 +1,72 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@components/ui/alert-dialog" +import { ReloadIcon } from "@radix-ui/react-icons"; +import React from "react"; + + + +interface AlertDialogConfirmationProps { + title: string; + description: string; + confirmText?: string; + cancelText?: string; + onConfirm: () => void; + onCancel: () => void; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + loading?: boolean +} + +export function AlertDialogConfirmation({ + title, + description, + confirmText = "Continue", + cancelText = "Cancel", + onConfirm, + onCancel, + isOpen, + onOpenChange, + loading +}: AlertDialogConfirmationProps) { + return ( + + + + {title} + {description} + + + + {cancelText} + + + {loading && ( + + )} + {loading ? "Processing..." : confirmText} + + + + + ); +} diff --git a/apps/web/lib/components/index.ts b/apps/web/lib/components/index.ts index c9f001e4a..356270efd 100644 --- a/apps/web/lib/components/index.ts +++ b/apps/web/lib/components/index.ts @@ -30,3 +30,4 @@ export * from './inputs/auth-code-input'; export * from './services/recaptcha'; export * from './copy-tooltip'; export * from './alert-popup' +export * from './alert-dialog-confirmation' 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 83468b5bb..bd937ee60 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -44,7 +44,7 @@ import { MdKeyboardArrowRight } from "react-icons/md" import { ConfirmStatusChange, StatusBadge, statusOptions, dataSourceTimeSheet, TimeSheet } from "." -import { useModal } from "@app/hooks" +import { useModal, useTimelogFilterOptions } from "@app/hooks" import { Checkbox } from "@components/ui/checkbox" import { Accordion, @@ -53,12 +53,12 @@ import { AccordionTrigger, } from "@components/ui/accordion" import { clsxm } from "@/app/utils" -import { statusColor } from "@/lib/components" +import { AlertDialogConfirmation, statusColor } from "@/lib/components" import { Badge } from '@components/ui/badge' -import { EditTaskModal, RejectSelectedModal, StatusType, getTimesheetButtons } from "@/app/[locale]/timesheet/[memberId]/components" +import { EditTaskModal, RejectSelectedModal, StatusAction, StatusType, getTimesheetButtons } from "@/app/[locale]/timesheet/[memberId]/components" import { useTranslations } from "next-intl" import { formatDate } from "@/app/helpers" -import { GroupedTimesheet } from "@/app/hooks/features/useTimesheet" +import { GroupedTimesheet, useTimesheet } from "@/app/hooks/features/useTimesheet" @@ -178,6 +178,26 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { openModal, closeModal } = useModal(); + const { deleteTaskTimesheet, loadingDeleteTimesheet } = useTimesheet({}) + const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet } = useTimelogFilterOptions() + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const handleConfirm = () => { + try { + deleteTaskTimesheet() + .then(() => { + setSelectTimesheet([]) + setIsDialogOpen(false); + }) + .catch((error) => { + console.error('Delete timesheet error:', error); + }); + } catch (error) { + console.error('Delete timesheet error:', error); + } + }; + const handleCancel = () => { + setIsDialogOpen(false); + }; const t = useTranslations(); const [sorting, setSorting] = React.useState([]) @@ -209,7 +229,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { Rejected: table.getRowModel().rows.filter(row => row.original.status === "Rejected") }; - const handleButtonClick = (action: StatusType) => { + const handleButtonClick = (action: StatusAction) => { switch (action) { case 'Approved': // TODO: Implement approval logic @@ -217,8 +237,8 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { case 'Rejected': openModal() break; - case 'Pending': - // TODO: Implement pending logic + case 'Deleted': + setIsDialogOpen(true) break; default: console.error(`Unsupported action: ${action}`); @@ -227,6 +247,17 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { return (
+ { // Pending implementation @@ -283,7 +314,10 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { style={{ backgroundColor: statusColor(status).bgOpacity, borderBottomColor: statusColor(status).bg }} className={clsxm("flex items-center border-b border-b-gray-200 dark:border-b-gray-600 space-x-4 p-1 h-[60px]")} > - + handleSelectRowTimesheet(task.id)} + checked={selectTimesheet.includes(task.id)} + />
{/* {/* Planned Time */} diff --git a/apps/web/package.json b/apps/web/package.json index 7e36362dc..32e4781f6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -52,6 +52,7 @@ "@opentelemetry/semantic-conventions": "^1.18.1", "@popperjs/core": "^2.11.6", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", @@ -98,7 +99,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.42", "nanoid": "5.0.1", - "next": "14.2.17", + "next": "14.2.17", "next-auth": "^5.0.0-beta.18", "next-intl": "^3.3.2", "next-themes": "^0.2.1", @@ -132,9 +133,6 @@ "tailwind-merge": "^1.14.0" }, "devDependencies": { - "tailwindcss-animate": "^1.0.6", - "tailwindcss": "^3.4.1", - "postcss": "^8.4.19", "@svgr/webpack": "^8.1.0", "@tailwindcss/typography": "^0.5.9", "@types/cookie": "^0.5.1", @@ -151,6 +149,9 @@ "eslint": "^8.28.0", "eslint-config-next": "^14.0.4", "eslint-plugin-unused-imports": "^3.0.0", + "postcss": "^8.4.19", + "tailwindcss": "^3.4.1", + "tailwindcss-animate": "^1.0.6", "typescript": "^4.9.4" }, "prettier": { diff --git a/yarn.lock b/yarn.lock index d416b8b87..6e68a578d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6050,6 +6050,18 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-alert-dialog@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz#ac3bb7f71f5cbb595d3d0949bb12b598c2a99981" + integrity sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dialog" "1.1.2" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-arrow@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" @@ -6203,7 +6215,7 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.4" -"@radix-ui/react-dialog@^1.1.2": +"@radix-ui/react-dialog@1.1.2", "@radix-ui/react-dialog@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== From 0fdda8aa2cd9eea4305c1e870dd43ac5fd66ab46 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:18:55 +0200 Subject: [PATCH 15/41] [Feat]: Display Timesheet Data and Refactor Code (#3342) * feat: display timesheet data and refactor code * fix: codeRabbit * Update apps/web/lib/features/task/task-card.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Ruslan Konviser Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../[memberId]/components/TimesheetAction.tsx | 28 +-- .../components/TimesheetFilterDate.tsx | 2 +- apps/web/app/hooks/features/useTimesheet.ts | 58 +++++- apps/web/app/interfaces/ITask.ts | 8 + apps/web/app/interfaces/timer/ITimerLog.ts | 110 ++++++---- .../services/client/api/timer/timer-log.ts | 8 +- .../app/services/server/requests/timesheet.ts | 6 +- apps/web/app/stores/time-logs.ts | 4 +- apps/web/lib/components/types.ts | 30 ++- .../calendar/table-time-sheet.tsx | 196 ++++++++++-------- apps/web/lib/features/task/task-card.tsx | 54 ++--- apps/web/lib/features/task/task-issue.tsx | 18 +- 12 files changed, 316 insertions(+), 206 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx index 213b9ccc0..5e1de381d 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx @@ -8,12 +8,15 @@ type ITimesheetButton = { title?: string, onClick?: () => void, className?: string, - icon?: ReactNode + icon?: ReactNode, + disabled?: boolean } -export const TimesheetButton = ({ className, icon, onClick, title }: ITimesheetButton) => { +export const TimesheetButton = ({ className, icon, onClick, title, disabled }: ITimesheetButton) => { return ( -
) } -
+ {isVisible &&
}
{[ t('common.FILTER_TODAY'), diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 959f2ccdd..3352f9861 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -5,7 +5,7 @@ import { useQuery } from '../useQuery'; import { useCallback, useEffect } from 'react'; import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log'; import moment from 'moment'; -import { ITimeSheet } from '@/app/interfaces'; +import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; import { useTimelogFilterOptions } from './useTimelogFilterOptions'; interface TimesheetParams { @@ -15,25 +15,35 @@ interface TimesheetParams { export interface GroupedTimesheet { date: string; - tasks: ITimeSheet[]; + tasks: TimesheetLog[]; } + + interface DeleteTimesheetParams { organizationId: string; tenantId: string; logIds: string[]; } -const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => { + +const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => { if (!items?.length) return []; - type GroupedMap = Record; + type GroupedMap = Record; + const groupedByDate = items.reduce((acc, item) => { - if (!item?.createdAt) return acc; + if (!item?.timesheet?.createdAt) { + console.warn('Skipping item with missing timesheet or createdAt:', item); + return acc; + } try { - const date = new Date(item.createdAt).toISOString().split('T')[0]; + const date = new Date(item.timesheet.createdAt).toISOString().split('T')[0]; if (!acc[date]) acc[date] = []; acc[date].push(item); } catch (error) { - console.error('Invalid date format:', item.createdAt); + console.error( + `Failed to process date for timesheet ${item.timesheet.id}:`, + { createdAt: item.timesheet.createdAt, error } + ); } return acc; }, {}); @@ -83,6 +93,37 @@ export function useTimesheet({ ] ); + const getStatusTimesheet = (items: TimesheetLog[] = []) => { + const STATUS_MAP: Record = { + PENDING: [], + APPROVED: [], + DENIED: [], + DRAFT: [], + 'IN REVIEW': [] + }; + + return items.reduce((acc, item) => { + const status = item.timesheet.status; + if (isTimesheetStatus(status)) { + acc[status].push(item); + } else { + console.warn(`Invalid timesheet status: ${status}`); + } + return acc; + }, STATUS_MAP); + } + + // Type guard + function isTimesheetStatus(status: unknown): status is TimesheetStatus { + const timesheetStatusValues: TimesheetStatus[] = [ + "DRAFT", + "PENDING", + "IN REVIEW", + "DENIED", + "APPROVED" + ]; + return Object.values(timesheetStatusValues).includes(status as TimesheetStatus); + } const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => { @@ -126,6 +167,7 @@ export function useTimesheet({ timesheet: groupByDate(timesheet), getTaskTimesheet, loadingDeleteTimesheet, - deleteTaskTimesheet + deleteTaskTimesheet, + getStatusTimesheet }; } diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts index daa407299..0bb4459b3 100644 --- a/apps/web/app/interfaces/ITask.ts +++ b/apps/web/app/interfaces/ITask.ts @@ -137,6 +137,14 @@ export type ITaskStatusField = | 'tags' | 'status type'; +export type TimesheetStatus = + | "DRAFT" + | "PENDING" + | "IN REVIEW" + | "DENIED" + | "APPROVED"; + + export type ITaskStatusStack = { status: ITaskStatus; size: ITaskSize; diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 3e30da712..d6e469d25 100644 --- a/apps/web/app/interfaces/timer/ITimerLog.ts +++ b/apps/web/app/interfaces/timer/ITimerLog.ts @@ -1,66 +1,99 @@ -import { ITaskIssue } from ".."; +import { ITeamTask } from "../ITask"; -interface Project { +interface BaseEntity { id: string; - name: string; - imageUrl: string; - membersCount: number; - image: string | null; -} - -interface Task { - id: string; - title: string; - issueType?: ITaskIssue | null; - estimate: number | null; - taskStatus: string | null; - taskNumber: string; + isActive: boolean; + isArchived: boolean; + tenantId: string; + organizationId: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + archivedAt: string | null; } -interface OrganizationContact { - id: string; - name: string; - imageUrl: string; +interface ImageEntity { + imageUrl: string | null; image: string | null; } -interface User { - id: string; +interface User extends BaseEntity { firstName: string; lastName: string; - imageUrl: string; - image: string | null; name: string; + imageUrl: string | null; + image: string | null; } -interface Employee { - id: string; +interface Employee extends BaseEntity { isOnline: boolean; isAway: boolean; user: User; fullName: string; } -export interface ITimeSheet { - deletedAt: string | null; - id: string; - createdAt: string; - updatedAt: string; - isActive: boolean; - isArchived: boolean; - archivedAt: string | null; - tenantId: string; - organizationId: string; +interface TaskStatus extends BaseEntity { + name: string; + value: string; + description: string; + order: number; + icon: string; + color: string; + isSystem: boolean; + isCollapsed: boolean; + isDefault: boolean; + isTodo: boolean; + isInProgress: boolean; + isDone: boolean; + projectId: string | null; + organizationTeamId: string | null; + fullIconUrl: string; +} +interface Task extends ITeamTask { + taskStatus: TaskStatus | null, + number: number; + description: string; + startDate: string | null; +} + + +interface Timesheet extends BaseEntity { + duration: number; + keyboard: number; + mouse: number; + overall: number; + startedAt: string; + stoppedAt: string; + approvedAt: string | null; + submittedAt: string | null; + lockedAt: string | null; + editedAt: string | null; + isBilled: boolean; + status: string; + employeeId: string; + approvedById: string | null; + isEdited: boolean; +} +interface Project extends BaseEntity, ImageEntity { + name: string; + membersCount: number; +} + +interface OrganizationContact extends BaseEntity, ImageEntity { + name: string; +} + +export interface TimesheetLog extends BaseEntity { startedAt: string; stoppedAt: string; editedAt: string | null; - logType: string; - source: string; + logType: "TRACKED" | "MANUAL"; + source: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP"; description: string; reason: string | null; isBillable: boolean; isRunning: boolean; - version: number | null; + version: string | null; employeeId: string; timesheetId: string; projectId: string; @@ -71,6 +104,7 @@ export interface ITimeSheet { task: Task; organizationContact: OrganizationContact; employee: Employee; + timesheet: Timesheet, duration: number; isEdited: boolean; } diff --git a/apps/web/app/services/client/api/timer/timer-log.ts b/apps/web/app/services/client/api/timer/timer-log.ts index 3715f8cc3..b35b7abea 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -1,4 +1,4 @@ -import { ITimeSheet, ITimerStatus } from '@app/interfaces'; +import { TimesheetLog, ITimerStatus } from '@app/interfaces'; import { get, deleteApi } from '../../axios'; export async function getTimerLogs( @@ -53,7 +53,9 @@ export async function getTaskTimesheetLogsApi({ 'relations[1]': 'task', 'relations[2]': 'organizationContact', 'relations[3]': 'employee.user', - 'relations[4]': 'task.taskStatus' + 'relations[4]': 'task.taskStatus', + 'relations[5]': 'timesheet' + }); projectIds.forEach((id, index) => { @@ -64,7 +66,7 @@ export async function getTaskTimesheetLogsApi({ params.append(`employeeIds[${index}]`, id); }); const endpoint = `/timesheet/time-log?${params.toString()}`; - return get(endpoint, { tenantId }); + return get(endpoint, { tenantId }); } diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index 1c5fe6048..4040697dd 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -1,7 +1,7 @@ import { ITasksTimesheet } from '@app/interfaces/ITimer'; import { serverFetch } from '../fetch'; import qs from 'qs'; -import { ITimeSheet } from '@/app/interfaces/timer/ITimerLog'; +import { TimesheetLog } from '@/app/interfaces/timer/ITimerLog'; export type TTasksTimesheetStatisticsParams = { tenantId: string; @@ -72,7 +72,7 @@ type ITimesheetProps = { export function getTaskTimesheetRequest(params: ITimesheetProps, bearer_token: string) { const queries = qs.stringify(params); - return serverFetch({ + return serverFetch({ path: `/timesheet/time-log?activityLevel?${queries.toString()}`, method: 'GET', bearer_token, @@ -88,7 +88,7 @@ type IDeleteTimesheetProps = { export function deleteTaskTimesheetRequest(params: IDeleteTimesheetProps, bearer_token: string) { const { logIds = [] } = params; - return serverFetch({ + return serverFetch({ path: `/timesheet/time-log/${logIds.join(',')}`, method: 'DELETE', bearer_token, diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index e73d66c64..d015ab9bf 100644 --- a/apps/web/app/stores/time-logs.ts +++ b/apps/web/app/stores/time-logs.ts @@ -1,6 +1,6 @@ import { ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs'; import { atom } from 'jotai'; -import { IProject, ITeamTask, ITimeSheet, OT_Member } from '../interfaces'; +import { IProject, ITeamTask, OT_Member, TimesheetLog } from '../interfaces'; interface IFilterOption { value: string; @@ -9,7 +9,7 @@ interface IFilterOption { export const timerLogsDailyReportState = atom([]); -export const timesheetRapportState = atom([]) +export const timesheetRapportState = atom([]) export const timesheetFilterEmployeeState = atom([]); export const timesheetFilterProjectState = atom([]); diff --git a/apps/web/lib/components/types.ts b/apps/web/lib/components/types.ts index 30755c619..30c0412ef 100644 --- a/apps/web/lib/components/types.ts +++ b/apps/web/lib/components/types.ts @@ -6,21 +6,37 @@ type StatusColorScheme = { }; const STATUS_COLORS: Record = { - Pending: { + PENDING: { bg: 'bg-[#FBB650]', text: 'text-[#FBB650]', - bgOpacity: 'rgba(251, 182, 80, 0.1)' + bgOpacity: 'rgba(251, 182, 80, 0.1)', }, - Approved: { + APPROVED: { bg: 'bg-[#30B366]', text: 'text-[#30B366]', - bgOpacity: 'rgba(48, 179, 102, 0.1)' + bgOpacity: 'rgba(48, 179, 102, 0.1)', }, - Rejected: { + DENIED: { bg: 'bg-[#dc2626]', text: 'text-[#dc2626]', - bgOpacity: 'rgba(220, 38, 38, 0.1)' - } + bgOpacity: 'rgba(220, 38, 38, 0.1)', + }, + DRAFT: { + bg: 'bg-gray-300', + text: 'text-gray-500', + bgOpacity: 'rgba(220, 220, 220, 0.1)', + }, + 'IN REVIEW': { + bg: 'bg-blue-500', + text: 'text-blue-500', + bgOpacity: 'rgba(59, 130, 246, 0.1)', + }, + DEFAULT: { + bg: 'bg-gray-100', + text: 'text-gray-400', + bgOpacity: 'rgba(243, 244, 246, 0.1)', + }, + }; 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 bd937ee60..24f5d3202 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -24,10 +24,6 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@components/ui/dropdown-menu" -import { - Table, - TableBody -} from "@components/ui/table" import { Select, SelectContent, @@ -59,8 +55,9 @@ import { EditTaskModal, RejectSelectedModal, StatusAction, StatusType, getTimesh import { useTranslations } from "next-intl" import { formatDate } from "@/app/helpers" import { GroupedTimesheet, useTimesheet } from "@/app/hooks/features/useTimesheet" - - +import { TaskNameInfoDisplay } from "../../task/task-displays" +import { TimesheetStatus } from "@/app/interfaces" +import dayjs from 'dayjs'; export const columns: ColumnDef[] = [ @@ -178,7 +175,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { openModal, closeModal } = useModal(); - const { deleteTaskTimesheet, loadingDeleteTimesheet } = useTimesheet({}) + const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet } = useTimesheet({}) const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet } = useTimelogFilterOptions() const [isDialogOpen, setIsDialogOpen] = React.useState(false); const handleConfirm = () => { @@ -198,7 +195,6 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { const handleCancel = () => { setIsDialogOpen(false); }; - const t = useTranslations(); const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) @@ -223,18 +219,13 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { }, }) - const groupedRows = { - Pending: table.getRowModel().rows.filter(row => row.original.status === "Pending"), - Approved: table.getRowModel().rows.filter(row => row.original.status === "Approved"), - Rejected: table.getRowModel().rows.filter(row => row.original.status === "Rejected") - }; const handleButtonClick = (action: StatusAction) => { switch (action) { case 'Approved': // TODO: Implement approval logic break; - case 'Rejected': + case 'Denied': openModal() break; case 'Deleted': @@ -268,82 +259,93 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { isOpen={isOpen} />
- - - {data?.map((plan, index) => ( -
-
- {formatDate(plan?.date)} - 64:30h -
+ {data?.map((plan, index) => ( +
+
+ {formatDate(plan.date)} + 64:30h +
+ + + {Object.entries(getStatusTimesheet(plan.tasks)).map(([status, rows]) => ( + + +
+
+
+
+ + {status === 'DENIED' ? "REJECTED" : status} + + ({rows?.length}) +
+ + Total + 24:30h + +
+
+ {getTimesheetButtons(status as StatusType, t, true, handleButtonClick)} +
+
+
+ + {rows?.map((task) => ( +
+ handleSelectRowTimesheet(task.id)} + checked={selectTimesheet.includes(task.id)} + /> +
+ +
+ {task.project && task.project.name} +
+ + {task.employee.fullName} +
+
+ + {task.timesheet.status} + - - {Object.entries(groupedRows).map(([status, rows]) => ( - - -
-
-
-
- - {status} - - ({rows.length}) -
- - Total - 24:30h - -
-
- {getTimesheetButtons(status as StatusType, t, handleButtonClick)} -
-
- - {plan.tasks?.map((task) => ( -
- handleSelectRowTimesheet(task.id)} - checked={selectTimesheet.includes(task.id)} - /> -
- {/* */} -
- {task.isActive} - {task.employee.fullName} - - {/* {task.}h, {task.estimateDays}j, {task.estimateMinutes}m */} - - -
- ))} -
-
- ))} -
-
- ))} - -
+ + {dayjs(task.timesheet.createdAt).format("HH:mm:ss")} + + +
+ ))} + + + ))} + +
+ ))}
@@ -551,3 +553,21 @@ export const StatusTask = () => { ) } + + +const getBadgeColor = (timesheetStatus: TimesheetStatus | null) => { + switch (timesheetStatus) { + case 'DRAFT': + return 'bg-gray-300'; + case 'PENDING': + return 'bg-yellow-400'; + case 'IN REVIEW': + return 'bg-blue-500'; + case 'DENIED': + return 'bg-red-500'; + case 'APPROVED': + return 'bg-green-500'; + default: + return 'bg-gray-100'; + } +}; diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index c4d97f026..ae4d67e6f 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -646,39 +646,39 @@ function TaskCardMenu({ {(viewType == 'default' || (viewType === 'dailyplan' && planMode === 'Outstanding')) && ( - <> - -
- {!taskPlannedToday && ( -
  • - -
  • - )} - {!taskPlannedTomorrow && ( + <> + +
    + {!taskPlannedToday && ( +
  • + +
  • + )} + {!taskPlannedTomorrow && ( +
  • + +
  • + )}
  • - )} -
  • - -
  • -
    - - )} +
    + + )} {viewType === 'dailyplan' && (planMode === 'Today Tasks' || planMode === 'Future Tasks') && ( diff --git a/apps/web/lib/features/task/task-issue.tsx b/apps/web/lib/features/task/task-issue.tsx index 1b494a2f8..8c13db8ed 100644 --- a/apps/web/lib/features/task/task-issue.tsx +++ b/apps/web/lib/features/task/task-issue.tsx @@ -1,5 +1,5 @@ import { useModal } from '@app/hooks'; -import { IClassName, IssueType, ITaskIssue, ITeamTask, ITimeSheet, Nullable } from '@app/interfaces'; +import { IClassName, IssueType, ITaskIssue, ITeamTask, Nullable } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { BackButton, Button, Card, InputField, Modal, Text } from 'lib/components'; import { NoteIcon, BugIcon, Square4StackIcon, Square4OutlineIcon } from 'assets/svg'; @@ -212,19 +212,3 @@ export function CreateTaskIssueModal({ open, closeModal }: { open: boolean; clos ); } - - -export function TaskIssueStatusTimesheet({ - task, - className, - showIssueLabels -}: { task: Nullable; showIssueLabels?: boolean } & IClassName) { - return ( - - ); -} From 628a39e3e22f2872c5e5ed54e71ff733006d606a Mon Sep 17 00:00:00 2001 From: syns Date: Tue, 19 Nov 2024 02:20:08 +0700 Subject: [PATCH 16/41] Fix/server web application menu (#3341) * fix: server web application menu * fix: server web default updater setting * fix: server web platform logo dark and light * fix: server web host not trusted * fix: remove unused variable --- .../src/locales/i18n/bg/translation.json | 15 + .../src/locales/i18n/en/translation.json | 15 + .../helpers/services/libs/desktop-store.ts | 40 ++- apps/server-web/src/main/main.ts | 43 ++- apps/server-web/src/main/main_.ts | 137 --------- apps/server-web/src/main/menu.ts | 290 +++++++----------- .../components/svgs/EverTeamsLogo.tsx | 8 +- apps/web/app/constants.ts | 2 + apps/web/auth.ts | 2 + 9 files changed, 188 insertions(+), 364 deletions(-) delete mode 100644 apps/server-web/src/main/main_.ts diff --git a/apps/server-web/src/locales/i18n/bg/translation.json b/apps/server-web/src/locales/i18n/bg/translation.json index a9ba889cc..bcec1eff5 100644 --- a/apps/server-web/src/locales/i18n/bg/translation.json +++ b/apps/server-web/src/locales/i18n/bg/translation.json @@ -14,6 +14,21 @@ "OPEN_WEB": "Отворете уеб в браузъра", "SERVER_WINDOW": "Прозорец на сървъра" }, + "MENU_APP": { + "ABOUT": "Относно", + "QUIT": "Изход", + "WINDOW": "Прозорец", + "SUBMENU": { + "SETTING": "Настройки", + "SERVER_WINDOW": "Сървърен прозорец", + "LEARN_MORE": "Научете повече", + "DOC": "Документация", + "SETTING_DEV": "Настройки за разработчици", + "SERVER_DEV": "Сървър за разработчици" + }, + "DEV": "Разработчик", + "HELP": "Помощ" + }, "FORM": { "FIELDS": { "PORT": "ПРИСТАНИЩЕ", diff --git a/apps/server-web/src/locales/i18n/en/translation.json b/apps/server-web/src/locales/i18n/en/translation.json index 48320d87b..c18265dde 100644 --- a/apps/server-web/src/locales/i18n/en/translation.json +++ b/apps/server-web/src/locales/i18n/en/translation.json @@ -14,6 +14,21 @@ "OPEN_WEB": "Open Web In Browser", "SERVER_WINDOW": "Server Window" }, + "MENU_APP": { + "ABOUT": "About", + "QUIT": "Quit", + "WINDOW": "Window", + "SUBMENU": { + "SETTING": "Setting", + "SERVER_WINDOW": "Server Window", + "LEARN_MORE": "Learn More", + "DOC": "Documentation", + "SETTING_DEV": "Setting Dev.", + "SERVER_DEV": "Server Dev." + }, + "DEV": "Developer", + "HELP": "Help" + }, "FORM": { "FIELDS": { "PORT": "PORT", diff --git a/apps/server-web/src/main/helpers/services/libs/desktop-store.ts b/apps/server-web/src/main/helpers/services/libs/desktop-store.ts index 9db836cd9..01c708563 100644 --- a/apps/server-web/src/main/helpers/services/libs/desktop-store.ts +++ b/apps/server-web/src/main/helpers/services/libs/desktop-store.ts @@ -1,6 +1,19 @@ import Store from 'electron-store'; import { WebServer } from '../../interfaces'; const store = new Store(); +const DEFAULT_CONFIG:any = { + server: { + PORT: 3002, + GAUZY_API_SERVER_URL: 'http://localhost:3000', + NEXT_PUBLIC_GAUZY_API_SERVER_URL: 'http://localhost:3000', + DESKTOP_WEB_SERVER_HOSTNAME: '0.0.0.0' + }, + general: { + lang: 'en', + autoUpdate: true, + updateCheckPeriode: '1140' + } +} export const LocalStore = { getStore: (source: string | 'config'): WebServer | any => { return store.get(source); @@ -24,22 +37,15 @@ export const LocalStore = { setDefaultServerConfig: () => { - const defaultConfig: WebServer | any = store.get('config'); - if (!defaultConfig || !defaultConfig.server || !defaultConfig.general) { - const config: WebServer = { - server: { - PORT: 3002, - GAUZY_API_SERVER_URL: 'http://localhost:3000', - NEXT_PUBLIC_GAUZY_API_SERVER_URL: 'http://localhost:3000', - DESKTOP_WEB_SERVER_HOSTNAME: '0.0.0.0' - }, - general: { - lang: 'en', - autoUpdate: true, - updateCheckPeriode: '30' - } - } - store.set({ config }); - } + const defaultConfig: WebServer | any = store.get('config') || {}; + Object.keys(DEFAULT_CONFIG).forEach((key) => { + Object.keys(DEFAULT_CONFIG[key]).forEach((keySub) => { + defaultConfig[key] = defaultConfig[key] || {}; + defaultConfig[key][keySub] = defaultConfig[key][keySub] || DEFAULT_CONFIG[key][keySub]; + }) + }) + store.set({ + config: defaultConfig + }); } }; diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index f88b5cf7b..cba85f718 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { app, ipcMain, Tray, dialog, BrowserWindow, shell } from 'electron'; +import { app, ipcMain, Tray, dialog, BrowserWindow, shell, Menu } from 'electron'; import { DesktopServer } from './helpers/desktop-server'; import { LocalStore } from './helpers/services/libs/desktop-store'; import { EventEmitter } from 'events'; @@ -23,8 +23,6 @@ Object.assign(console, Log.functions); app.name = config.DESCRIPTION; - - const eventEmitter = new EventEmitter(); const controller = new AbortController(); @@ -43,6 +41,7 @@ let logWindow: BrowserWindow | null = null; let setupWindow: BrowserWindow | any = null; let SettingMenu: any = null; let ServerWindowMenu: any = null; +const appMenu = new MenuBuilder(eventEmitter) Log.hooks.push((message: any, transport) => { if (transport !== Log.transports.file) { @@ -93,6 +92,7 @@ i18nextMainBackend.on('initialized', () => { }); let trayMenuItems: any = []; +let appMenuItems: any = []; const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'assets') @@ -182,10 +182,7 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO settingWindow = null; SettingMenu = null }); - if (!SettingMenu) { - SettingMenu = new MenuBuilder(settingWindow); - } - SettingMenu.buildMenu(); + Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) break; case 'LOG_WINDOW': logWindow = new BrowserWindow(defaultOptionWindow); @@ -196,10 +193,7 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO logWindow = null; ServerWindowMenu = null }) - if (!ServerWindowMenu) { - ServerWindowMenu = new MenuBuilder(logWindow); - } - ServerWindowMenu.buildMenu(); + Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) break; case 'SETUP_WINDOW': setupWindow = new BrowserWindow(defaultOptionWindow); @@ -218,7 +212,7 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO const runServer = async () => { console.log('Run the Server...'); try { - const envVal = getEnvApi(); + const envVal: any = getEnvApi(); // Instantiate API and UI servers await desktopServer.start( @@ -262,14 +256,18 @@ const SendMessageToSettingWindow = (type: string, data: any) => { } const onInitApplication = () => { - LocalStore.setDefaultServerConfig(); // check and set default config + // check and set default config + LocalStore.setDefaultServerConfig(); createIntervalAutoUpdate() trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); + appMenuItems = appMenuItems.length ? appMenuItems : appMenu.defaultMenu(); tray = _initTray(trayMenuItems, getAssetPath('icons/icon.png')); i18nextMainBackend.on('languageChanged', (lng) => { if (i18nextMainBackend.isInitialized) { + trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); updateTrayMenu('none', {}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) } }); eventEmitter.on(EventLists.webServerStart, async () => { @@ -425,6 +423,16 @@ const onInitApplication = () => { const url = `http://127.0.0.1:${envConfig?.PORT}` shell.openExternal(url) }) + + eventEmitter.on(EventLists.SETTING_WINDOW_DEV, () => { + settingWindow?.webContents.toggleDevTools(); + }) + + eventEmitter.on(EventLists.SERVER_WINDOW_DEV, () => { + logWindow?.webContents.toggleDevTools(); + }) + + eventEmitter.emit(EventLists.SERVER_WINDOW); } (async () => { @@ -452,7 +460,6 @@ ipcMain.on('message', async (event, arg) => { }) ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { - console.log('main setting page', arg); switch (arg.type) { case SettingPageTypeMessage.saveSetting: const existingConfig = getEnvApi(); @@ -512,13 +519,6 @@ ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { case SettingPageTypeMessage.themeChange: eventEmitter.emit(EventLists.CHANGE_THEME, arg.data) break; - default: - break; - } -}) - -ipcMain.on(IPC_TYPES.UPDATER_PAGE, (event, arg) => { - switch (arg.type) { case SettingPageTypeMessage.updateSetting: LocalStore.updateConfigSetting({ general: { @@ -529,7 +529,6 @@ ipcMain.on(IPC_TYPES.UPDATER_PAGE, (event, arg) => { createIntervalAutoUpdate() event.sender.send(IPC_TYPES.UPDATER_PAGE, { type: SettingPageTypeMessage.updateSettingResponse, data: true }) break; - default: break; } diff --git a/apps/server-web/src/main/main_.ts b/apps/server-web/src/main/main_.ts deleted file mode 100644 index 10c8220ed..000000000 --- a/apps/server-web/src/main/main_.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint global-require: off, no-console: off, promise/always-return: off */ - -/** - * This module executes inside of electron's main process. You can start - * electron renderer process from here and communicate with the other processes - * through IPC. - * - * When running `npm run build` or `npm run build:main`, this file is compiled to - * `./src/main.js` using webpack. This gives us some performance wins. - */ -import path from 'path'; -import { app, BrowserWindow, shell, ipcMain } from 'electron'; -import { autoUpdater } from 'electron-updater'; -import log from 'electron-log'; -import MenuBuilder from './menu'; -import { resolveHtmlPath } from './util'; - -class AppUpdater { - constructor() { - log.transports.file.level = 'info'; - autoUpdater.logger = log; - autoUpdater.checkForUpdatesAndNotify(); - } -} - -let mainWindow: BrowserWindow | null = null; - -ipcMain.on('ipc-example', async (event, arg) => { - const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`; - console.log(msgTemplate(arg)); - event.reply('ipc-example', msgTemplate('pong')); -}); - -if (process.env.NODE_ENV === 'production') { - const sourceMapSupport = require('source-map-support'); - sourceMapSupport.install(); -} - -const isDebug = - process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; - -if (isDebug) { - require('electron-debug')(); -} - -const installExtensions = async () => { - const installer = require('electron-devtools-installer'); - const forceDownload = !!process.env.UPGRADE_EXTENSIONS; - const extensions = ['REACT_DEVELOPER_TOOLS']; - - return installer - .default( - extensions.map((name) => installer[name]), - forceDownload, - ) - .catch(console.log); -}; - -const createWindow = async () => { - if (isDebug) { - await installExtensions(); - } - - const RESOURCES_PATH = app.isPackaged - ? path.join(process.resourcesPath, 'assets') - : path.join(__dirname, '../../assets'); - - const getAssetPath = (...paths: string[]): string => { - return path.join(RESOURCES_PATH, ...paths); - }; - - mainWindow = new BrowserWindow({ - show: false, - width: 1024, - height: 728, - icon: getAssetPath('icon.png'), - webPreferences: { - preload: app.isPackaged - ? path.join(__dirname, 'preload.js') - : path.join(__dirname, '../../.erb/dll/preload.js'), - }, - }); - - mainWindow.loadURL(resolveHtmlPath('index.html')); - - mainWindow.on('ready-to-show', () => { - if (!mainWindow) { - throw new Error('"mainWindow" is not defined'); - } - if (process.env.START_MINIMIZED) { - mainWindow.minimize(); - } else { - mainWindow.show(); - } - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); - - const menuBuilder = new MenuBuilder(mainWindow); - menuBuilder.buildMenu(); - - // Open urls in the user's browser - mainWindow.webContents.setWindowOpenHandler((eData) => { - shell.openExternal(eData.url); - return { action: 'deny' }; - }); - - // Remove this if your app does not use auto updates - // eslint-disable-next-line - new AppUpdater(); -}; - -/** - * Add event listeners... - */ - -app.on('window-all-closed', () => { - // Respect the OSX convention of having the application in memory even - // after all windows have been closed - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -app - .whenReady() - .then(() => { - createWindow(); - app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (mainWindow === null) createWindow(); - }); - }) - .catch(console.log); diff --git a/apps/server-web/src/main/menu.ts b/apps/server-web/src/main/menu.ts index be4348ceb..cec72f148 100644 --- a/apps/server-web/src/main/menu.ts +++ b/apps/server-web/src/main/menu.ts @@ -2,216 +2,79 @@ import { app, Menu, shell, - BrowserWindow, - MenuItemConstructorOptions, } from 'electron'; import { config } from '../configs/config'; - -interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { - selector?: string; - submenu?: DarwinMenuItemConstructorOptions[] | Menu; -} +import { EventEmitter } from 'events'; +import { EventLists } from './helpers/constant'; +import i18n from 'i18next'; export default class MenuBuilder { - mainWindow: BrowserWindow; - - constructor(mainWindow: BrowserWindow) { - this.mainWindow = mainWindow; - } - - buildMenu(): Menu { - if ( - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ) { - this.setupDevelopmentEnvironment(); - } - - const template = - process.platform === 'darwin' - ? this.buildDarwinTemplate() - : this.buildDefaultTemplate(); - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - - return menu; - } - - setupDevelopmentEnvironment(): void { - this.mainWindow.webContents.on('context-menu', (_, props) => { - const { x, y } = props; - - Menu.buildFromTemplate([ - { - label: 'Inspect element', - click: () => { - this.mainWindow.webContents.inspectElement(x, y); - }, - }, - ]).popup({ window: this.mainWindow }); - }); - } + eventEmitter: EventEmitter - buildDarwinTemplate(): MenuItemConstructorOptions[] { - const subMenuAbout: DarwinMenuItemConstructorOptions = { - label: config.DESCRIPTION || app.getName(), - submenu: [ - { - label: `About ${config.DESCRIPTION || app.getName()}`, - selector: 'orderFrontStandardAboutPanel:', - }, - { type: 'separator' }, - { - label: 'Quit', - accelerator: 'Command+Q', - click: () => { - app.quit(); - }, - }, - ], - }; - const subMenuViewDev: MenuItemConstructorOptions = { - label: 'View', - submenu: [ - { - label: 'Reload', - accelerator: 'Command+R', - click: () => { - this.mainWindow.webContents.reload(); - }, - }, - { - label: 'Toggle Developer Tools', - accelerator: 'Alt+Command+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ], - }; - const subMenuViewProd: MenuItemConstructorOptions = { - label: 'View', - submenu: [ - { - label: 'Reload', - accelerator: 'Command+R', - click: () => { - this.mainWindow.webContents.reload(); - }, - }, - { - label: 'Toggle Developer Tools', - accelerator: 'Alt+Command+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ], - }; - const subMenuWindow: DarwinMenuItemConstructorOptions = { - label: 'Window', - submenu: [ - { - label: 'Minimize', - accelerator: 'Command+M', - selector: 'performMiniaturize:', - }, - { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, - { type: 'separator' }, - ], - }; - const subMenuHelp: MenuItemConstructorOptions = { - label: 'Help', - submenu: [ - { - label: 'Learn More', - click() { - shell.openExternal('https://ever.team/'); - }, - }, - { - label: 'Documentation', - click() { - shell.openExternal( - 'https://github.com/ever-co/ever-teams/blob/develop/README.md', - ); - }, - }, - { - label: 'Search Issues', - click() { - shell.openExternal('https://github.com/ever-co/ever-teams/issues'); - }, - }, - ], - }; - - const subMenuView = - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ? subMenuViewDev - : subMenuViewProd; - - return [subMenuAbout, subMenuView, subMenuWindow, subMenuHelp]; + constructor(eventEmitter: EventEmitter) { + this.eventEmitter = eventEmitter } - buildDefaultTemplate() { - const templateDefault = [ + defaultMenu() { + const isDarwin = process.platform === 'darwin'; + return [ { - label: '&File', + id: 'MENU_APP', + label: config.DESCRIPTION || app.getName(), submenu: [ { - label: '&Close', - accelerator: 'Ctrl+W', + id: 'MENU_APP_ABOUT', + label: `MENU_APP.ABOUT`, + selector: 'orderFrontStandardAboutPanel:', + click: () => { + this.eventEmitter.emit(EventLists.gotoAbout) + } + }, + { type: 'separator' }, + { + id: 'MENU_APP_QUIT', + label: 'MENU_APP.QUIT', + accelerator: isDarwin ? 'Command+Q' : 'Alt+F4', click: () => { - this.mainWindow.close(); + app.quit(); }, }, ], }, { - label: '&View', - submenu: - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ? [ - { - label: '&Reload', - accelerator: 'Ctrl+R', - click: () => { - this.mainWindow.webContents.reload(); - }, - }, - { - label: 'Toggle &Developer Tools', - accelerator: 'Alt+Ctrl+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ] - : [ - { - label: 'Toggle &Developer Tools', - accelerator: 'Alt+Ctrl+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ], + id: 'MENU_APP_WINDOW', + label: 'MENU_APP.WINDOW', + submenu: [ + { + id: 'SUBMENU_SETTING', + label: 'MENU_APP.SUBMENU.SETTING', + click: () => { + this.eventEmitter.emit(EventLists.gotoSetting); + } + }, + { + id: 'SUBMENU_SERVER', + label: 'MENU_APP.SUBMENU.SERVER_WINDOW', + click: () => { + this.eventEmitter.emit(EventLists.SERVER_WINDOW); + } + } + ] }, { - label: 'Help', + id: 'MENU_APP_HELP', + label: 'MENU_APP.HELP', submenu: [ { - label: 'Learn More', + id: 'SUBMENU_LEARN_MORE', + label: 'MENU_APP.SUBMENU.LEARN_MORE', click() { shell.openExternal(config.COMPANY_SITE_LINK); }, }, { - label: 'Documentation', + id: 'SUBMENU_DOC', + label: 'MENU_APP.SUBMENU.DOC', click() { shell.openExternal( config.COMPANY_GITHUB_LINK @@ -220,8 +83,61 @@ export default class MenuBuilder { }, ], }, - ]; + { + id: 'MENU_APP_DEV', + label: 'MENU_APP.DEV', + submenu: [ + { + id: 'SUBMENU_SETTING_DEV', + label: 'MENU_APP.SUBMENU.SETTING_DEV', + click: () => { + this.eventEmitter.emit(EventLists.SETTING_WINDOW_DEV); + }, + }, + { + id: 'SUBMENU_SERVER_DEV', + label: 'MENU_APP.SUBMENU.SERVER_DEV', + click: () => { + this.eventEmitter.emit(EventLists.SERVER_WINDOW_DEV); + }, + }, + ] + }, + ] + } - return templateDefault; + buildDefaultTemplate(menuItems: any, i18nextMainBackend: typeof i18n) { + return Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, menuItems)); } + + updateAppMenu(menuItem: string, context: { label?: string, enabled?: boolean}, contextMenuItems: any, i18nextMainBackend: typeof i18n) { + const menuIdx:number = contextMenuItems.findIndex((item: any) => item.id === menuItem); + if (menuIdx > -1) { + contextMenuItems[menuIdx] = {...contextMenuItems[menuIdx], ...context}; + const newMenu = [...contextMenuItems]; + Menu.setApplicationMenu(Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, newMenu))); + } else { + const newMenu = [...contextMenuItems]; + Menu.setApplicationMenu(Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, newMenu))) + } +} + +translateAppMenu(i18nextMainBackend: typeof i18n, contextMenu: any) { + return contextMenu.map((menu: any) => { + const menuCopied = {...menu}; + if (menuCopied.label) { + menuCopied.label = i18nextMainBackend.t(menuCopied.label); + } + if (menuCopied.submenu && menuCopied.submenu.length) { + menuCopied.submenu = menuCopied.submenu.map((sm: any) => { + const submenu = {...sm}; + if (submenu.label) { + submenu.label = i18nextMainBackend.t(submenu.label) + } + return submenu; + }) + } + return menuCopied; + }) +} } diff --git a/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx b/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx index 2a123bf40..3df194cda 100644 --- a/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx +++ b/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx @@ -1,6 +1,12 @@ import Logo from '../../../resources/icons/platform-logo.png'; export const EverTeamsLogo = () => { return ( - EverTeams Logo + EverTeams Logo ); }; diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index db8321a56..e2a2e0564 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -317,6 +317,8 @@ export const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET; export const TWITTER_CLIENT_ID = process.env.TWITTER_CLIENT_ID; export const TWITTER_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET; +export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true'; + // Add manual timer reason export const manualTimeReasons: ManualTimeReasons[] = [ diff --git a/apps/web/auth.ts b/apps/web/auth.ts index 56e936665..36430b60a 100644 --- a/apps/web/auth.ts +++ b/apps/web/auth.ts @@ -2,9 +2,11 @@ import NextAuth from 'next-auth'; import { filteredProviders } from '@app/utils/check-provider-env-vars'; import { GauzyAdapter, jwtCallback, ProviderEnum, signInCallback } from '@app/services/server/requests/OAuth'; import { NextRequest } from 'next/server'; +import { IS_DESKTOP_APP } from '@app/constants'; export const { handlers, signIn, signOut, auth } = NextAuth((request) => ({ providers: filteredProviders, + trustHost: IS_DESKTOP_APP, adapter: GauzyAdapter(request as NextRequest), session: { strategy: 'jwt' }, callbacks: { From ae9cb254aa212a31805ecb7dc0f056458e71a489 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Mon, 18 Nov 2024 21:21:07 +0200 Subject: [PATCH 17/41] feat: add table view for daily plan tasks (#3335) --- apps/web/components/ui/data-table.tsx | 6 +- .../features/task/daily-plan/future-tasks.tsx | 220 ++++++++++-------- .../features/task/daily-plan/past-tasks.tsx | 158 +++++++------ .../cells/task-action-menu-cell.tsx | 51 ++++ .../table-view/cells/task-estimation-cell.tsx | 24 ++ .../table-view/cells/task-info-cell.tsx | 7 + .../table-view/cells/task-times-cell.tsx | 31 +++ .../task/daily-plan/table-view/index.tsx | 75 ++++++ apps/web/lib/features/task/task-card.tsx | 4 +- .../lib/features/team-members-table-view.tsx | 1 - apps/web/lib/features/user-profile-plans.tsx | 163 +++++++------ 11 files changed, 486 insertions(+), 254 deletions(-) create mode 100644 apps/web/lib/features/task/daily-plan/table-view/cells/task-action-menu-cell.tsx create mode 100644 apps/web/lib/features/task/daily-plan/table-view/cells/task-estimation-cell.tsx create mode 100644 apps/web/lib/features/task/daily-plan/table-view/cells/task-info-cell.tsx create mode 100644 apps/web/lib/features/task/daily-plan/table-view/cells/task-times-cell.tsx create mode 100644 apps/web/lib/features/task/daily-plan/table-view/index.tsx diff --git a/apps/web/components/ui/data-table.tsx b/apps/web/components/ui/data-table.tsx index 09c74e340..8a9ceaf8e 100644 --- a/apps/web/components/ui/data-table.tsx +++ b/apps/web/components/ui/data-table.tsx @@ -86,9 +86,9 @@ function DataTable({ columns, data, footerRows, isHeader }: DataT {header.isPlaceholder ? null : flexRender( - header.column.columnDef.header, - header.getContext() - )} + header.column.columnDef.header, + header.getContext() + )}
    diff --git a/apps/web/lib/features/task/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index 0b5f6ac59..65cb07acf 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -16,6 +16,7 @@ import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; import { IDailyPlan } from '@app/interfaces'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; +import DailyPlanTasksTableView from './table-view'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); @@ -57,115 +58,130 @@ export function FutureTasks({ profile }: { profile: any }) { - - {(provided) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} - <>{provided.placeholder} - {canSeeActivity ? ( -
      - + ) : ( + + {(provided) => ( +
        + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
        + +
        + )} +
        + ) : ( + + {(provided) => ( +
        + +
        + )} +
        + ) + )} + <>{provided.placeholder} + {canSeeActivity ? ( +
        + { + setPopupOpen((prev) => !prev); + setCurrentDeleteIndex(index); + }} + variant="outline" + className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" + > + Delete this plan + + } + > + {/*button confirm*/} + {/*button cancel*/} + - } - > - {/*button confirm*/} - - {/*button cancel*/} - - -
        - ) : ( - <> - )} -
      - )} -
      +
      +
      + ) : ( + <> + )} +
    + )} +
    + )}
    ))} diff --git a/apps/web/lib/features/task/daily-plan/past-tasks.tsx b/apps/web/lib/features/task/daily-plan/past-tasks.tsx index 29ce71c3e..d4fdf8cd1 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -13,6 +13,7 @@ import { useEffect, useState } from 'react'; import { IDailyPlan } from '@app/interfaces'; import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; +import DailyPlanTasksTableView from './table-view'; export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { const { pastPlans } = useDailyPlan(); @@ -51,79 +52,90 @@ export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any {/* Plan header */} - - {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} -
    - )} -
    + {view === 'TABLE' ? ( + + ) : ( + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
      + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ) + )} +
    + )} +
    + )}
    ))} diff --git a/apps/web/lib/features/task/daily-plan/table-view/cells/task-action-menu-cell.tsx b/apps/web/lib/features/task/daily-plan/table-view/cells/task-action-menu-cell.tsx new file mode 100644 index 000000000..89643f9df --- /dev/null +++ b/apps/web/lib/features/task/daily-plan/table-view/cells/task-action-menu-cell.tsx @@ -0,0 +1,51 @@ +import { ITeamTask } from '@/app/interfaces'; +import { CellContext } from '@tanstack/react-table'; +import { ActiveTaskStatusDropdown } from '../../../task-status'; +import { useMemo, useState } from 'react'; +import { I_UserProfilePage, useOrganizationTeams, useTeamMemberCard } from '@/app/hooks'; +import { get } from 'lodash'; +import { TaskCardMenu } from '../../../task-card'; + +export default function TaskActionMenuCell(props: CellContext) { + const [loading, setLoading] = useState(false); + const { activeTeam } = useOrganizationTeams(); + const members = useMemo(() => activeTeam?.members || [], [activeTeam?.members]); + const profile = get(props.column, 'columnDef.meta.profile') as unknown as I_UserProfilePage; + const plan = get(props.column, 'columnDef.meta.plan'); + const planMode = get(props.column, 'columnDef.meta.planMode'); + const currentMember = useMemo( + () => + members.find((m) => { + return m.employee.user?.id === profile?.userProfile?.id; + }), + [members, profile?.userProfile?.id] + ); + const memberInfo = useTeamMemberCard(currentMember || undefined); + + return ( +
    + {/* Active Task Status Dropdown (It's a dropdown that allows the user to change the status of the task.)*/} +
    + setLoading(load)} + className="min-w-[10.625rem] text-sm" + /> +
    + {/* TaskCardMenu */} +
    + {props.row.original && currentMember && ( + + )} +
    +
    + ); +} diff --git a/apps/web/lib/features/task/daily-plan/table-view/cells/task-estimation-cell.tsx b/apps/web/lib/features/task/daily-plan/table-view/cells/task-estimation-cell.tsx new file mode 100644 index 000000000..51a933455 --- /dev/null +++ b/apps/web/lib/features/task/daily-plan/table-view/cells/task-estimation-cell.tsx @@ -0,0 +1,24 @@ +import { I_UserProfilePage, useOrganizationTeams, useTeamMemberCard, useTMCardTaskEdit } from '@/app/hooks'; +import { ITeamTask } from '@/app/interfaces'; +import { TaskEstimateInfo } from '@/lib/features/team/user-team-card/task-estimate'; +import { CellContext } from '@tanstack/react-table'; +import { get } from 'lodash'; +import { useMemo } from 'react'; + +export default function DailyPlanTaskEstimationCell(props: CellContext) { + const plan = get(props.column, 'columnDef.meta.plan'); + const profile = get(props.column, 'columnDef.meta.profile') as unknown as I_UserProfilePage; + const { activeTeam } = useOrganizationTeams(); + const members = useMemo(() => activeTeam?.members || [], [activeTeam?.members]); + const currentMember = useMemo( + () => + members.find((m) => { + return m.employee.user?.id === profile?.userProfile?.id; + }), + [members, profile?.userProfile?.id] + ); + const memberInfo = useTeamMemberCard(currentMember || undefined); + const taskEdition = useTMCardTaskEdit(props.row.original); + + return ; +} diff --git a/apps/web/lib/features/task/daily-plan/table-view/cells/task-info-cell.tsx b/apps/web/lib/features/task/daily-plan/table-view/cells/task-info-cell.tsx new file mode 100644 index 000000000..d31827ce7 --- /dev/null +++ b/apps/web/lib/features/task/daily-plan/table-view/cells/task-info-cell.tsx @@ -0,0 +1,7 @@ +import { CellContext } from '@tanstack/react-table'; +import { TaskInfo } from '../../../task-card'; +import { ITeamTask } from '@/app/interfaces'; + +export default function DailyPlanTaskInfoCell(props: CellContext) { + return ; +} diff --git a/apps/web/lib/features/task/daily-plan/table-view/cells/task-times-cell.tsx b/apps/web/lib/features/task/daily-plan/table-view/cells/task-times-cell.tsx new file mode 100644 index 000000000..2ba60368f --- /dev/null +++ b/apps/web/lib/features/task/daily-plan/table-view/cells/task-times-cell.tsx @@ -0,0 +1,31 @@ +import { CellContext } from '@tanstack/react-table'; +import { TaskTimes } from '../../../task-times'; +import { ITeamTask } from '@/app/interfaces'; +import { I_UserProfilePage, useOrganizationTeams, useTeamMemberCard } from '@/app/hooks'; +import get from 'lodash/get'; +import { useMemo } from 'react'; + +export default function DailyPlanTaskTimesCell(props: CellContext) { + const profile = get(props.column, 'columnDef.meta.profile') as unknown as I_UserProfilePage; + const { activeTeam } = useOrganizationTeams(); + const members = useMemo(() => activeTeam?.members || [], [activeTeam?.members]); + const currentMember = useMemo( + () => + members.find((m) => { + return m.employee.user?.id === profile?.userProfile?.id; + }), + [members, profile?.userProfile?.id] + ); + const memberInfo = useTeamMemberCard(currentMember || undefined); + + return ( + + ); +} diff --git a/apps/web/lib/features/task/daily-plan/table-view/index.tsx b/apps/web/lib/features/task/daily-plan/table-view/index.tsx new file mode 100644 index 000000000..7adc2dc79 --- /dev/null +++ b/apps/web/lib/features/task/daily-plan/table-view/index.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import DataTable from '@components/ui/data-table'; +import { ColumnDef } from '@tanstack/react-table'; +import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import DailyPlanTaskEstimationCell from './cells/task-estimation-cell'; +import DailyPlanTaskInfoCell from './cells/task-info-cell'; +import DailyPlanTaskTimesCell from './cells/task-times-cell'; +import TaskActionMenuCell from './cells/task-action-menu-cell'; +import { FilterTabs, I_UserProfilePage } from '@/app/hooks'; + +interface IDailyPlanTasksTableViewProps { + data: ITeamTask[]; + plan: IDailyPlan; + profile: I_UserProfilePage; + planMode?: FilterTabs; +} + +/** + * Table view of daily plan tasks + * + * @param {Object} props - THe props object + * @param {ITeamTask[]} props.data - The tasks + * @param {I_UserProfilePage} props.profile - The user profile page + * @param {FilterTabs} props.planMode - The plan mode to display + * + * @returns {JSX.Element} - The table view of daily plan tasks + */ +export default function DailyPlanTasksTableView(props: IDailyPlanTasksTableViewProps) { + const { data, plan, profile, planMode } = props; + + const columns = React.useMemo[]>( + () => [ + { + id: 'task', + header: 'Task', + tooltip: '', + cell: DailyPlanTaskInfoCell + }, + { + id: 'estimate', + header: 'Estimate', + tooltip: '', + cell: DailyPlanTaskEstimationCell, + meta: { + plan, + profile + } + }, + { + id: 'workedOn', + header: 'Worked On', + tooltip: '', + cell: DailyPlanTaskTimesCell, + meta: { + profile + } + }, + { + id: 'action', + header: 'Action', + tooltip: '', + cell: TaskActionMenuCell, + meta: { + plan, + profile, + planMode + } + } + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ; +} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index ae4d67e6f..3eae62ec9 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -480,7 +480,7 @@ function TimerButtonCall({ //* Task Estimate info * //* Task Info FC * -function TaskInfo({ +export function TaskInfo({ className, task, taskBadgeClassName, @@ -526,7 +526,7 @@ function TaskInfo({ /** * It's a dropdown menu that allows the user to remove the task. */ -function TaskCardMenu({ +export function TaskCardMenu({ task, loading, memberInfo, diff --git a/apps/web/lib/features/team-members-table-view.tsx b/apps/web/lib/features/team-members-table-view.tsx index af2d3d471..50df9227d 100644 --- a/apps/web/lib/features/team-members-table-view.tsx +++ b/apps/web/lib/features/team-members-table-view.tsx @@ -72,7 +72,6 @@ const TeamMembersTableView = ({ return ( <> - []} diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index fbdac86f3..754622dd6 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -50,6 +50,7 @@ import TaskBlockCard from './task/task-block-card'; import { TaskCard } from './task/task-card'; import moment from 'moment'; import { usePathname } from 'next/navigation'; +import DailyPlanTasksTableView from './task/daily-plan/table-view'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; @@ -333,79 +334,95 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current - - {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} - <>{provided.placeholder} -
    - )} -
    + + {view === 'TABLE' ? ( + + ) : ( + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
      + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ) + )} + <>{provided.placeholder} +
    + )} +
    + )}
    ))} From 5b1a6bd28db8c0da67f27969b06cb0b421c16e4e Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Mon, 18 Nov 2024 21:25:11 +0200 Subject: [PATCH 18/41] improvement: status type | edit/create mode (#3185) * improvement: status type | edit/create mode * fix svg icon colors * fix svg icon default value * adapt requested changes * add requested changes * fix build errors --- .vscode/settings.json | 4 +- apps/web/app/constants.ts | 2 + apps/web/app/services/server/fetch.ts | 39 ++++++++- apps/web/lib/features/task/task-status.tsx | 9 +- apps/web/lib/settings/icon-items.tsx | 9 +- apps/web/lib/settings/list-card.tsx | 56 +++++++++--- apps/web/lib/settings/task-statuses-form.tsx | 91 ++++++++++++++++---- apps/web/locales/ar.json | 2 + apps/web/locales/bg.json | 2 + apps/web/locales/de.json | 2 + apps/web/locales/en.json | 2 + apps/web/locales/es.json | 2 + apps/web/locales/fr.json | 2 + apps/web/locales/he.json | 2 + apps/web/locales/it.json | 2 + apps/web/locales/nl.json | 2 + apps/web/locales/pl.json | 2 + apps/web/locales/pt.json | 2 + apps/web/locales/ru.json | 2 + apps/web/locales/zh.json | 2 + 20 files changed, 196 insertions(+), 40 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 06dae99cd..be7e65f57 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,9 +23,7 @@ }, "vsicons.presets.angular": true, "deepscan.enable": true, - "cSpell.words": [ - "Timepicker" - ], + "cSpell.words": ["Timepicker"], "files.exclude": { "**/.git": true, "**/.DS_Store": true, diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index e2a2e0564..b7d0ad636 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -209,6 +209,8 @@ export const TaskStatus = { INPROGRESS: 'in-progress' }; +export const tasksStatusSvgCacheDuration = 1000 * 60 * 60; + export const languagesFlags = [ { Flag: US, diff --git a/apps/web/app/services/server/fetch.ts b/apps/web/app/services/server/fetch.ts index 32d56cb05..f026969ed 100644 --- a/apps/web/app/services/server/fetch.ts +++ b/apps/web/app/services/server/fetch.ts @@ -1,4 +1,4 @@ -import { GAUZY_API_SERVER_URL } from '@app/constants'; +import { GAUZY_API_SERVER_URL, tasksStatusSvgCacheDuration } from '@app/constants'; export function serverFetch({ path, @@ -54,3 +54,40 @@ export function serverFetch({ }; }); } + +/** Tasks status SVG icons fetch */ + +// In memory cache for performance + +const tasksStatusSvgCache = new Map< + string, + { + content: Response; + timestamp: number; + } +>(); + +export async function svgFetch(url: string): Promise { + try { + //Url validation + new URL(url); + + const cached = tasksStatusSvgCache.get(url); + const now = Date.now(); + + if (cached && now - cached.timestamp < tasksStatusSvgCacheDuration) { + return cached.content.clone(); + } + + // Fetch the SVG + const response = await fetch(url); + + tasksStatusSvgCache.set(url, { + content: response.clone(), + timestamp: now + }); + return response; + } catch { + throw new Error('Invalid URL provided'); + } +} diff --git a/apps/web/lib/features/task/task-status.tsx b/apps/web/lib/features/task/task-status.tsx index 73096654c..eb9c4e2d7 100644 --- a/apps/web/lib/features/task/task-status.tsx +++ b/apps/web/lib/features/task/task-status.tsx @@ -33,6 +33,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline'; import { readableColor } from 'polished'; import { useTheme } from 'next-themes'; import { Square4OutlineIcon, CircleIcon } from 'assets/svg'; +import { getTextColor } from '@app/helpers'; import { cn } from '@/lib/utils'; export type TStatusItem = { @@ -854,7 +855,8 @@ export function TaskStatus({ className )} style={{ - backgroundColor: active ? backgroundColor : undefined + backgroundColor: active ? backgroundColor : undefined, + color: getTextColor(backgroundColor ?? 'white') }} >
    ({ className={clsxm( `justify-between capitalize`, sidebarUI && ['text-xs'], - !value && ['text-dark dark:text-white dark:bg-dark--theme-light'], + !value && ['!text-dark/40 dark:text-white'], isVersion || (forDetails && !value) ? 'bg-transparent border border-solid border-color-[#F2F2F2]' - : 'bg-[#F2F2F2] ', + : 'bg-white border', 'dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33]', taskStatusClassName, isVersion && 'dark:text-white', @@ -1028,6 +1030,7 @@ export function StatusDropdown({ 10} label={capitalize(value?.name) || ''} + className="h-full" > {button} diff --git a/apps/web/lib/settings/icon-items.tsx b/apps/web/lib/settings/icon-items.tsx index 5ba87b2db..8c72290e2 100644 --- a/apps/web/lib/settings/icon-items.tsx +++ b/apps/web/lib/settings/icon-items.tsx @@ -2,6 +2,7 @@ import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { IIcon } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { DropdownItem } from 'lib/components'; +import { useTranslations } from 'next-intl'; import Image from 'next/image'; export type IconItem = DropdownItem; @@ -53,6 +54,7 @@ export function IconItem({ url: string; disabled?: boolean; }) { + const t = useTranslations(); return (
    - {url && ( + {url ? (
    + ) : ( + {t('common.ICON')} )}
    - - {title} -
    ); } diff --git a/apps/web/lib/settings/list-card.tsx b/apps/web/lib/settings/list-card.tsx index f07ef63b0..e9ad47a24 100644 --- a/apps/web/lib/settings/list-card.tsx +++ b/apps/web/lib/settings/list-card.tsx @@ -1,11 +1,12 @@ import { EditPenUnderlineIcon, TrashIcon } from 'assets/svg'; import { Button, Text, Tooltip } from 'lib/components'; -import Image from 'next/image'; import { CHARACTER_LIMIT_TO_SHOW } from '@app/constants'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { getTextColor } from '@app/helpers'; import { useTranslations } from 'next-intl'; +import { useEffect } from 'react'; +import { svgFetch } from '@app/services/server/fetch'; export const StatusesListCard = ({ statusIcon, @@ -27,6 +28,12 @@ export const StatusesListCard = ({ const textColor = getTextColor(bgColor); const t = useTranslations(); + useEffect(() => { + if (statusIcon) { + loadSVG(statusIcon, 'icon-container' + statusTitle, textColor); + } + }, [statusIcon, statusTitle, textColor]); + return (
    - {statusIcon && ( - {statusTitle} - )} + {statusIcon &&
    } = CHARACTER_LIMIT_TO_SHOW} @@ -84,3 +80,37 @@ export const StatusesListCard = ({
    ); }; + +/** + * A function to load an SVG and gives the ability to + * update its attributes. e.g: fill color + * + * @param {string} url the URL of the SVG file to load + * @param {string} containerId the ID of the container where the SVG will be inserted + * @param {string} color the fill color for the SVG + */ +const loadSVG = async (url: string, containerId: string, color: string): Promise => { + try { + const response = await svgFetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.statusText}`); + } + + let svgContent = await response.text(); + + // Update the fill color in the SVG content + svgContent = svgContent.replace(/stroke="[^"]*"/g, `stroke="${color}"`); + + const container = document.getElementById(containerId); + + if (container) { + console.log(container); + container.innerHTML = svgContent; + } else { + console.error(`Container with ID "${containerId}" not found.`); + } + } catch (error) { + console.error(`Error loading SVG: ${(error as Error).message}`); + } +}; diff --git a/apps/web/lib/settings/task-statuses-form.tsx b/apps/web/lib/settings/task-statuses-form.tsx index 678e9b569..c07e48b7b 100644 --- a/apps/web/lib/settings/task-statuses-form.tsx +++ b/apps/web/lib/settings/task-statuses-form.tsx @@ -6,7 +6,7 @@ import { clsxm } from '@app/utils'; import { Spinner } from '@components/ui/loaders/spinner'; import { PlusIcon } from '@heroicons/react/20/solid'; import { Button, ColorPicker, InputField, Modal, Text } from 'lib/components'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslations } from 'next-intl'; import { useAtom } from 'jotai'; @@ -31,6 +31,8 @@ export const TaskStatusesForm = ({ const [createNew, setCreateNew] = useState(formOnly); const [edit, setEdit] = useState(null); const t = useTranslations(); + const [selectedStatusType, setSelectedStatusType] = useState(null); + const [randomColor, setRandomColor] = useState(undefined); const taskStatusIconList: IIcon[] = generateIconList('task-statuses', [ 'open', @@ -38,14 +40,15 @@ export const TaskStatusesForm = ({ 'ready', 'in-review', 'blocked', - 'completed' + 'completed', + 'backlog', ]); const taskSizesIconList: IIcon[] = generateIconList('task-sizes', [ - 'x-large' - // 'large', - // 'medium', - // 'small', - // 'tiny', + 'x-large', + 'large', + 'medium', + 'small', + 'tiny', ]); const taskPrioritiesIconList: IIcon[] = generateIconList('task-priorities', [ 'urgent', @@ -54,11 +57,12 @@ export const TaskStatusesForm = ({ 'low' ]); - const iconList: IIcon[] = [ + const iconList: IIcon[] = useMemo(() => [ ...taskStatusIconList, ...taskSizesIconList, ...taskPrioritiesIconList - ]; + // eslint-disable-next-line react-hooks/exhaustive-deps + ],[]) ; const { loading, @@ -157,6 +161,53 @@ export const TaskStatusesForm = ({ const [statusToDelete, setStatusToDelete] = useState(null) const {tasks} = useTeamTasks() + /** + * Get Icon by status name + * + * @param {string} iconName - Name of the icon + * @returns {IIcon} - Icon of the status + */ + const getIcon = useCallback( + (iconName: string | null) => { + if (!iconName) return null; + + const STATUS_MAPPINGS: Record = { + 'ready-for-review': 'ready' + }; + + const name = STATUS_MAPPINGS[iconName] || iconName; + + const icon = iconList.find((icon) => icon.title === name); + + if (icon) { + setValue('icon', icon.path); + } + return icon; + }, + [iconList, setValue] + ); + + + /** + * Get random color for new status + * + * @returns {string} - Random color + */ + const getRandomColor = useCallback(() => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + }, []); + + useEffect(() => { + if (!edit && selectedStatusType) { + setRandomColor(getRandomColor()); + } + }, [selectedStatusType, edit, getRandomColor]); + return ( <> @@ -195,7 +246,7 @@ export const TaskStatusesForm = ({ variant="outline" className="rounded-[10px]" > - Sort + {t('common.SORT')}
    {(createNew || edit) && ( @@ -218,22 +269,26 @@ export const TaskStatusesForm = ({ {...register('name')} /> setValue('template', status)} - className=" h-14 shrink-0" + onValueChange={(status) => { + setValue('template', status) + setSelectedStatusType(status) + } } + className="h-14 shrink-0" + defaultValue={edit?.value} /> icon.path === edit.icon - ) as IIcon) - : null + ) as IIcon) : null } /> setValue('color', color)} className=" shrink-0" /> @@ -247,6 +302,9 @@ export const TaskStatusesForm = ({ createTaskStatusLoading || editTaskStatusLoading } loading={createTaskStatusLoading || editTaskStatusLoading} + onClick={() => { + setSelectedStatusType(null); + }} > {edit ? t('common.SAVE') : t('common.CREATE')} @@ -257,6 +315,7 @@ export const TaskStatusesForm = ({ onClick={() => { setCreateNew(false); setEdit(null); + setSelectedStatusType(null); }} > {t('common.CANCEL')} diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 77d575a46..2b45b804d 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -235,6 +235,8 @@ "CHANGE_RELATIONS": "تغيير العلاقات", "SET_AS_NEXT": "تعيين كالتالي", "MOVE_TO": "نقل إلى", + "SORT": "فرز", + "ICON": "أيقونة", "SELECT_DATE": "اختر التاريخ", "SELECT_AND_CLOSE": "اختر وأغلق", "SELECT_ROLE": "يرجى اختيار أي دور", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 146962a2e..6a35a5b89 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -255,6 +255,8 @@ "MOVE_TO": "Преместете в", "SELECT_DATE": "Изберете дата", "SELECT_AND_CLOSE": "Изберете и затворете", + "SORT": "Подредете", + "ICON": "Икона", "SELECT_ROLE": "Моля, изберете роля", "ADD_TIME": "Добавете време", "VIEW_TIMESHEET": "Преглед на работния час", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index c42de6e56..a8cd98b43 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -255,6 +255,8 @@ "MOVE_TO": "Verschieben nach", "SELECT_DATE": "Datum auswählen", "SELECT_AND_CLOSE": "Auswählen und schließen", + "SORT": "Sortieren", + "ICON": "Symbol", "SELECT_ROLE": "Bitte wählen Sie eine Rolle", "ADD_TIME": "Zeit hinzufügen", "VIEW_TIMESHEET": "Zeiterfassung anzeigen", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index a35d7ec92..e230c7a31 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -255,6 +255,8 @@ "MOVE_TO": "Move to", "SELECT_DATE": "Select date", "SELECT_AND_CLOSE": "Select & Close", + "SORT": "Sort", + "ICON": "Icon", "SELECT_ROLE": "Please Select any Role", "ADD_TIME": "Add Time", "VIEW_TIMESHEET": "View timesheet", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 8a30a3032..7e8997c94 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -255,6 +255,8 @@ "MOVE_TO": "Mover a", "SELECT_DATE": "Seleccionar fecha", "SELECT_AND_CLOSE": "Seleccionar y cerrar", + "SORT": "Ordenar", + "ICON": "Ícono", "SELECT_ROLE": "Por favor, selecciona un rol", "ADD_TIME": "Agregar tiempo", "VIEW_TIMESHEET": "Ver hoja de tiempo", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index bbd5f0430..2c70c0917 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -255,6 +255,8 @@ "MOVE_TO": "Déplacer vers", "SELECT_DATE": "Sélectionner la date", "SELECT_AND_CLOSE": "Sélectionner et fermer", + "SORT": "Trier", + "ICON": "Icône", "SELECT_ROLE": "Veuillez sélectionner un rôle", "ADD_TIME": "Ajouter du temps", "VIEW_TIMESHEET": "Voir la feuille de temps", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 0cf6d5c9e..e6c81b8fc 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -255,6 +255,8 @@ "MOVE_TO": "העבר אל", "SELECT_DATE": "בחר תאריך", "SELECT_AND_CLOSE": "בחר וסגור", + "SORT": "מיין", + "ICON": "סמל", "SELECT_ROLE": "אנא בחר תפקיד", "ADD_TIME": "הוסף זמן", "VIEW_TIMESHEET": "הצג גיליון זמן", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 576c9af34..945980bd8 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -255,6 +255,8 @@ "MOVE_TO": "Sposta a", "SELECT_DATE": "Seleziona data", "SELECT_AND_CLOSE": "Seleziona e chiudi", + "SORT": "Ordina", + "ICON": "Icona", "SELECT_ROLE": "Seleziona un ruolo", "ADD_TIME": "Aggiungi tempo", "VIEW_TIMESHEET": "Vedi foglio ore", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 1c7612d5c..2445311c8 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -255,6 +255,8 @@ "MOVE_TO": "Verplaatsen naar", "SELECT_DATE": "Selecteer datum", "SELECT_AND_CLOSE": "Selecteren en sluiten", + "SORT": "Sorteren", + "ICON": "Icoon", "SELECT_ROLE": "Selecteer een rol", "ADD_TIME": "Tijd toevoegen", "VIEW_TIMESHEET": "Bekijk tijdregistratie", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 7e45f58ba..198f39004 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -255,6 +255,8 @@ "MOVE_TO": "Przenieś do", "SELECT_DATE": "Wybierz datę", "SELECT_AND_CLOSE": "Wybierz i zamknij", + "SORT": "Sortuj", + "ICON": "Ikona", "SELECT_ROLE": "Proszę wybrać rolę", "ADD_TIME": "Dodaj czas", "VIEW_TIMESHEET": "Zobacz Ewidencję czasu", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 4a832f9ab..033cc1a79 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -255,6 +255,8 @@ "MOVE_TO": "Mover para", "SELECT_DATE": "Selecionar data", "SELECT_AND_CLOSE": "Selecionar e fechar", + "SORT": "Classificar", + "ICON": "Ícone", "SELECT_ROLE": "Por favor, selecione um cargo", "ADD_TIME": "Adicionar tempo", "VIEW_TIMESHEET": "Ver folha de ponto", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 20c2605ab..3ab812538 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -255,6 +255,8 @@ "MOVE_TO": "Переместить в", "SELECT_DATE": "Выберите дату", "SELECT_AND_CLOSE": "Выбрать и закрыть", + "SORT": "Сортировать", + "ICON": "Иконка", "SELECT_ROLE": "Пожалуйста, выберите роль", "ADD_TIME": "Добавить время", "VIEW_TIMESHEET": "Просмотреть табель", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 3735c7f97..50d39d2d2 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -255,6 +255,8 @@ "MOVE_TO": "移动到", "SELECT_DATE": "选择日期", "SELECT_AND_CLOSE": "选择并关闭", + "SORT": "排序", + "ICON": "图标", "SELECT_ROLE": "请选择角色", "ADD_TIME": "添加时间", "VIEW_TIMESHEET": "查看工时表", From f89d96dc7ca20cbc39607bbdebceb83037af794c Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:16:16 +0200 Subject: [PATCH 19/41] fix:bug (#3344) --- .cspell.json | 2 +- apps/web/app/interfaces/IDailyPlan.ts | 8 ++++---- apps/web/components/pages/kanban/menu-kanban-card.tsx | 5 ++--- .../features/daily-plan/create-daily-plan-form-modal.tsx | 8 ++++---- .../features/integrations/calendar/table-time-sheet.tsx | 4 ++-- apps/web/lib/features/task/task-card.tsx | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.cspell.json b/.cspell.json index be2df850e..9c47d01cc 100644 --- a/.cspell.json +++ b/.cspell.json @@ -372,7 +372,7 @@ "tinvitations", "tnode", "Togger", - "tomorow", + "tomorrow", "Tongatapu", "tota", "TRANSFERT", diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts index 8eba961df..e296af463 100644 --- a/apps/web/app/interfaces/IDailyPlan.ts +++ b/apps/web/app/interfaces/IDailyPlan.ts @@ -25,12 +25,12 @@ export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee, export interface IUpdateDailyPlan extends Partial, - Pick, - Partial> {} + Pick, + Partial> { } export interface IDailyPlanTasksUpdate extends Pick, - IBasePerTenantAndOrganizationEntity {} + IBasePerTenantAndOrganizationEntity { } export enum DailyPlanStatusEnum { OPEN = 'open', @@ -38,4 +38,4 @@ export enum DailyPlanStatusEnum { COMPLETED = 'completed' } -export type IDailyPlanMode = 'today' | 'tomorow' | 'custom'; +export type IDailyPlanMode = 'today' | 'tomorrow' | 'custom'; diff --git a/apps/web/components/pages/kanban/menu-kanban-card.tsx b/apps/web/components/pages/kanban/menu-kanban-card.tsx index 457433d6b..26ebbc833 100644 --- a/apps/web/components/pages/kanban/menu-kanban-card.tsx +++ b/apps/web/components/pages/kanban/menu-kanban-card.tsx @@ -170,7 +170,7 @@ export default function MenuKanbanCard({ item: task, member }: { item: ITeamTask
  • - +
  • @@ -227,8 +227,7 @@ function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element { - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active ? 'bg-primary/5' : 'text-gray-900' + `relative cursor-default select-none py-2 pl-10 pr-4 ${active ? 'bg-primary/5' : 'text-gray-900' }` } value={member} diff --git a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx index 2baacd152..147d9e459 100644 --- a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx +++ b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx @@ -91,7 +91,7 @@ export function CreateDailyPlanFormModal({ date: planMode == 'today' ? toDay - : planMode == 'tomorow' + : planMode == 'tomorrow' ? tomorrowDate : new Date(moment(date).format('YYYY-MM-DD')), status: DailyPlanStatusEnum.OPEN, @@ -302,12 +302,12 @@ function MembersList({ {(member?.employee?.user?.image?.thumbUrl || member?.employee?.user?.image?.fullUrl || member?.employee?.user?.imageUrl) && - isValidUrl( - member?.employee?.user?.image?.thumbUrl || + isValidUrl( + member?.employee?.user?.image?.thumbUrl || member?.employee?.user?.image?.fullUrl || member?.employee?.user?.imageUrl || '' - ) ? ( + ) ? ( )} - {planMode === 'tomorow' && !taskPlannedForTomorrow && ( + {planMode === 'tomorrow' && !taskPlannedForTomorrow && ( {isPending || createDailyPlanLoading ? ( From f37fac7c15c617b671b3ba9e1002b1affcd58439 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Tue, 19 Nov 2024 20:06:35 +0200 Subject: [PATCH 20/41] [Feature] Show | Edit project information in the task details page (#3347) * add project in task details page / possiblity to edit * Update apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Ruslan Konviser Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../api/organization-projects/[id]/route.ts | 21 +- .../app/api/organization-projects/route.ts | 19 +- .../hooks/features/useOrganizationProjects.ts | 122 +++-- apps/web/app/interfaces/ITask.ts | 2 +- .../client/api/organization-projects.ts | 28 +- .../server/requests/organization-projects.ts | 43 ++ apps/web/app/stores/organization-projects.ts | 4 + .../blocks/task-secondary-info.tsx | 444 +++++++++++------- apps/web/locales/ar.json | 3 +- apps/web/locales/bg.json | 3 +- apps/web/locales/de.json | 3 +- apps/web/locales/en.json | 3 +- apps/web/locales/es.json | 3 +- apps/web/locales/fr.json | 3 +- apps/web/locales/he.json | 3 +- apps/web/locales/it.json | 3 +- apps/web/locales/nl.json | 3 +- apps/web/locales/pl.json | 3 +- apps/web/locales/pt.json | 3 +- apps/web/locales/ru.json | 3 +- apps/web/locales/zh.json | 3 +- 21 files changed, 494 insertions(+), 228 deletions(-) create mode 100644 apps/web/app/stores/organization-projects.ts diff --git a/apps/web/app/api/organization-projects/[id]/route.ts b/apps/web/app/api/organization-projects/[id]/route.ts index 072bf41d9..12280aae7 100644 --- a/apps/web/app/api/organization-projects/[id]/route.ts +++ b/apps/web/app/api/organization-projects/[id]/route.ts @@ -1,6 +1,6 @@ import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app'; -import { editOrganizationProjectsRequest } from '@app/services/server/requests'; +import { editOrganizationProjectsRequest, getOrganizationProjectRequest } from '@app/services/server/requests'; import { NextResponse } from 'next/server'; export async function PUT(req: Request, { params }: { params: { id: string } }) { @@ -23,3 +23,22 @@ export async function PUT(req: Request, { params }: { params: { id: string } }) return $res(response.data); } + +export async function GET(req: Request, { params }: { params: { id: string } }) { + const res = new NextResponse(); + if (!params.id) { + return; + } + + const { $res, user, access_token, tenantId } = await authenticatedGuard(req, res); + + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const response = await getOrganizationProjectRequest({ + bearer_token: access_token, + id: params.id, + tenantId + }); + + return $res(response.data); +} diff --git a/apps/web/app/api/organization-projects/route.ts b/apps/web/app/api/organization-projects/route.ts index c76dfa9b0..b78c6d73c 100644 --- a/apps/web/app/api/organization-projects/route.ts +++ b/apps/web/app/api/organization-projects/route.ts @@ -1,6 +1,6 @@ import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app'; -import { createOrganizationProjectRequest } from '@app/services/server/requests'; +import { createOrganizationProjectRequest, getOrganizationProjectsRequest } from '@app/services/server/requests'; import { NextResponse } from 'next/server'; export async function POST(req: Request) { @@ -19,3 +19,20 @@ export async function POST(req: Request) { return $res(response.data); } + +export async function GET(req: Request) { + const res = new NextResponse(); + + const { $res, user, access_token, tenantId, organizationId } = await authenticatedGuard(req, res); + + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + + const response = await getOrganizationProjectsRequest({ + bearer_token: access_token, + tenantId, + organizationId + }); + + return $res(response.data); +} diff --git a/apps/web/app/hooks/features/useOrganizationProjects.ts b/apps/web/app/hooks/features/useOrganizationProjects.ts index fdacff18c..121c4dc8e 100644 --- a/apps/web/app/hooks/features/useOrganizationProjects.ts +++ b/apps/web/app/hooks/features/useOrganizationProjects.ts @@ -1,59 +1,83 @@ import { editOrganizationProjectSettingAPI, - editOrganizationProjectAPI + editOrganizationProjectAPI, + getOrganizationProjectAPI, + getOrganizationProjectsAPI } from '@app/services/client/api'; import { userState } from '@app/stores'; import { useCallback } from 'react'; import { useAtom } from 'jotai'; import { useQuery } from '../useQuery'; +import { organizationProjectsState } from '@/app/stores/organization-projects'; export function useOrganizationProjects() { - const [user] = useAtom(userState); - - const { - loading: editOrganizationProjectLoading, - queryCall: editOrganizationProjectQueryCall - } = useQuery(editOrganizationProjectAPI); - - const { - loading: editOrganizationProjectSettingLoading, - queryCall: editOrganizationProjectSettingQueryCall - } = useQuery(editOrganizationProjectSettingAPI); - - const editOrganizationProjectSetting = useCallback( - (id: string, data: any) => { - if (user?.tenantId) { - return editOrganizationProjectSettingQueryCall( - id, - data, - user?.tenantId || '' - ).then((res) => { - return res; - }); - } - }, - [user, editOrganizationProjectSettingQueryCall] - ); - - const editOrganizationProject = useCallback( - (id: string, data: any) => { - if (user?.tenantId) { - return editOrganizationProjectQueryCall( - id, - data, - user?.tenantId || '' - ).then((res) => { - return res; - }); - } - }, - [user, editOrganizationProjectQueryCall] - ); - - return { - editOrganizationProjectSetting, - editOrganizationProjectSettingLoading, - editOrganizationProject, - editOrganizationProjectLoading - }; + const [user] = useAtom(userState); + const [organizationProjects, setOrganizationProjects] = useAtom(organizationProjectsState); + + const { loading: editOrganizationProjectLoading, queryCall: editOrganizationProjectQueryCall } = + useQuery(editOrganizationProjectAPI); + + const { loading: editOrganizationProjectSettingLoading, queryCall: editOrganizationProjectSettingQueryCall } = + useQuery(editOrganizationProjectSettingAPI); + + const { loading: getOrganizationProjectLoading, queryCall: getOrganizationProjectQueryCall } = + useQuery(getOrganizationProjectAPI); + + const { loading: getOrganizationProjectsLoading, queryCall: getOrganizationProjectsQueryCall } = + useQuery(getOrganizationProjectsAPI); + + const editOrganizationProjectSetting = useCallback( + (id: string, data: any) => { + if (user?.tenantId) { + return editOrganizationProjectSettingQueryCall(id, data, user?.tenantId || '').then((res) => { + return res; + }); + } + }, + [user, editOrganizationProjectSettingQueryCall] + ); + + const editOrganizationProject = useCallback( + (id: string, data: any) => { + if (user?.tenantId) { + return editOrganizationProjectQueryCall(id, data, user?.tenantId || '').then((res) => { + return res; + }); + } + }, + [user, editOrganizationProjectQueryCall] + ); + + const getOrganizationProject = useCallback( + async (id: string) => { + try { + return await getOrganizationProjectQueryCall(id); + } catch (error) { + console.log(error); + } + }, + [getOrganizationProjectQueryCall] + ); + + const getOrganizationProjects = useCallback(async () => { + try { + const res = await getOrganizationProjectsQueryCall(); + + setOrganizationProjects(res.data.items); + } catch (error) { + console.log(error); + } + }, [getOrganizationProjectsQueryCall, setOrganizationProjects]); + + return { + editOrganizationProjectSetting, + editOrganizationProjectSettingLoading, + editOrganizationProject, + editOrganizationProjectLoading, + getOrganizationProject, + getOrganizationProjectLoading, + getOrganizationProjects, + getOrganizationProjectsLoading, + organizationProjects, + }; } diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts index 0bb4459b3..1a50b539c 100644 --- a/apps/web/app/interfaces/ITask.ts +++ b/apps/web/app/interfaces/ITask.ts @@ -18,7 +18,7 @@ export type ITeamTask = { estimateMinutes?: number; dueDate: string; startDate?: string | null; - projectId: string; + projectId: string | null; public: boolean; taskStatusId?: string; resolvedAt?: string; diff --git a/apps/web/app/services/client/api/organization-projects.ts b/apps/web/app/services/client/api/organization-projects.ts index 1f2d7243f..107225e44 100644 --- a/apps/web/app/services/client/api/organization-projects.ts +++ b/apps/web/app/services/client/api/organization-projects.ts @@ -1,5 +1,7 @@ -import { IProject } from '@app/interfaces'; -import { put } from '../axios'; +import { IProject, PaginationResponse } from '@app/interfaces'; +import { get, put } from '../axios'; +import qs from 'qs'; +import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers'; export function editOrganizationProjectSettingAPI(id: string, data: any, tenantId?: string) { return put(`/organization-projects/setting/${id}`, data, { @@ -12,3 +14,25 @@ export function editOrganizationProjectAPI(id: string, data: any, tenantId?: str tenantId }); } + +export function getOrganizationProjectAPI(id: string, tenantId?: string) { + return get(`/organization-projects/${id}`, { + tenantId + }); +} + +export function getOrganizationProjectsAPI() { + + const organizationId = getOrganizationIdCookie(); + const tenantId = getTenantIdCookie(); + + const obj = { + 'where[organizationId]': organizationId, + 'where[tenantId]': tenantId, + } + const query = qs.stringify(obj); + + return get>(`/organization-projects?${query}`, { + tenantId + }); +} diff --git a/apps/web/app/services/server/requests/organization-projects.ts b/apps/web/app/services/server/requests/organization-projects.ts index d4ce9f971..643995491 100644 --- a/apps/web/app/services/server/requests/organization-projects.ts +++ b/apps/web/app/services/server/requests/organization-projects.ts @@ -1,4 +1,6 @@ +import qs from 'qs'; import { serverFetch } from '../fetch'; +import { IProject, PaginationResponse } from '@/app/interfaces'; export function editOrganizationProjectsSettingsRequest({ id, @@ -39,3 +41,44 @@ export function editOrganizationProjectsRequest({ tenantId }); } + +export function getOrganizationProjectRequest({ + id, + tenantId, + bearer_token +}: { + id: string; + tenantId: string; + bearer_token: string; +}) { + return serverFetch({ + path: `/organization-projects/${id}`, + method: 'GET', + bearer_token, + tenantId + }); +} + +export function getOrganizationProjectsRequest({ + tenantId, + organizationId, + bearer_token +}: { + tenantId: string; + bearer_token: string; + organizationId : string; +}) { + + const obj = { + 'where[organizationId]': organizationId, + 'where[tenantId]': tenantId, + } + const query = qs.stringify(obj); + + return serverFetch>({ + path: `/organization-projects?${query}`, + method: 'GET', + bearer_token, + tenantId + }); +} diff --git a/apps/web/app/stores/organization-projects.ts b/apps/web/app/stores/organization-projects.ts new file mode 100644 index 000000000..e928707b5 --- /dev/null +++ b/apps/web/app/stores/organization-projects.ts @@ -0,0 +1,4 @@ +import { atom } from "jotai"; +import { IProject } from "../interfaces"; + +export const organizationProjectsState = atom([]) diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx index 687671af3..88c8690fd 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx @@ -1,8 +1,8 @@ -import { useModal, useTeamTasks } from '@app/hooks'; -import { ITaskVersionCreate, ITeamTask } from '@app/interfaces'; +import { useModal, useOrganizationProjects, useTeamTasks } from '@app/hooks'; +import { IProject, ITaskVersionCreate, ITeamTask } from '@app/interfaces'; import { detailedTaskState } from '@app/stores'; import { PlusIcon } from '@heroicons/react/20/solid'; -import { Button, Card, Modal, Tooltip } from 'lib/components'; +import { Button, Card, Modal, SpinnerLoader, Tooltip } from 'lib/components'; import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown, @@ -21,11 +21,14 @@ import { import { VersionForm } from 'lib/settings/version-form'; import { cloneDeep } from 'lodash'; import Link from 'next/link'; -import { useCallback, useMemo, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useAtomValue } from 'jotai'; import TaskRow from '../components/task-row'; import { useTranslations } from 'next-intl'; -import { Square4OutlineIcon } from 'assets/svg'; +import { ChevronDownIcon, Square4OutlineIcon } from 'assets/svg'; +import { Listbox, Transition } from '@headlessui/react'; +import { clsxm } from '@/app/utils'; +import { organizationProjectsState } from '@/app/stores/organization-projects'; type StatusType = 'version' | 'epic' | 'status' | 'label' | 'size' | 'priority'; @@ -89,163 +92,149 @@ const TaskSecondaryInfo = () => { }, [taskLabels, task?.tags]); return ( -
    - {/* Version */} - - - - - - - {/* Epic */} - {task && task.issueType === 'Story' && ( - - { - onTaskSelect({ - id: d - } as ITeamTask); - }} - className="lg:min-w-[170px] text-black" - forDetails={true} - sidebarUI={true} - taskStatusClassName="text-[0.625rem] h-[1.5625rem] max-w-[7.6875rem] rounded 3xl:text-xs" - defaultValue={task.parentId || ''} - /> - - )} - - {task && } - - {/* Task Status */} - - - - - - - {/* Task Labels */} - - - - {tags.length > 0 && ( - -
    - {tags.map((tag, i) => { - return ( - - - - ); - })} -
    -
    - )} +
    + {/* Version */} + + + + + - {/* Task Size */} - - - - - - - {/* Task Properties */} - - - - - - - - - {formTarget === 'version' && ( - - )} - {formTarget === 'status' && ( - - )} - {formTarget === 'priority' && ( - - )} - {formTarget === 'size' && ( - - )} - - -
    + {/* Epic */} + {task && task.issueType === 'Story' && ( + + { + onTaskSelect({ + id: d + } as ITeamTask); + }} + className="lg:min-w-[170px] text-black" + forDetails={true} + sidebarUI={true} + taskStatusClassName="text-[0.625rem] w-[7.6875rem] h-[2.35rem] max-w-[7.6875rem] rounded 3xl:text-xs" + defaultValue={task.parentId || ''} + /> + + )} + + {task && } + + {/* Task Status */} + + + + + + + {/* Task Labels */} + + + + {tags.length > 0 && ( + +
    + {tags.map((tag, i) => { + return ( + + + + ); + })} +
    +
    + )} + + {/* Task Size */} + + + + + + + {/* Task Properties */} + + + + + + + {/* Task project */} + {task && ( + + + + )} + + + {formTarget === 'version' && ( + + )} + {formTarget === 'status' && } + {formTarget === 'priority' && } + {formTarget === 'size' && } + + +
    ); }; @@ -280,4 +269,137 @@ const EpicParent = ({ task }: { task: ITeamTask }) => { ); }; + + +interface ITaskProjectDropdownProps { + task: ITeamTask; +} + export default TaskSecondaryInfo; + +/** + * TaskProject dropdown + * + * @param {Object} props - The props object + * @param {ITeamTask} props.task - The ITeamTask object which + * + * @returns {JSX.Element} - The Dropdown element + */ +function ProjectDropDown (props : ITaskProjectDropdownProps) { + + const {task} = props + + const organizationProjects = useAtomValue(organizationProjectsState) + const {getOrganizationProjects} = useOrganizationProjects() + const {updateTask, updateLoading} = useTeamTasks() + const t = useTranslations() + + useEffect(() => { + getOrganizationProjects() + },[getOrganizationProjects]) + + + const [selected, setSelected] = useState(); + + // Set the task project if any + useEffect(() => { + setSelected(organizationProjects.find(project => { + return project.id === task.projectId + })) + },[organizationProjects, task.projectId]) + + // Update the project + const handleUpdateProject = useCallback(async (project : IProject) => { + try { + await updateTask({ ...task, projectId: project.id }); + + setSelected(project); + } catch (error) { + console.error(error); + } + },[task, updateTask]) + + + // Remove the project + const handleRemoveProject = useCallback(async () => { + try { + await updateTask({ ...task, projectId: null }); + + setSelected(undefined); + } catch (error) { + console.error(error); + } + }, [task, updateTask]); + + return ( +
    + + {({ open }) => { + return ( + <> + + {updateLoading ? ( + + ) : ( +

    {selected?.name ?? 'Project'}

    + )} +
    + + + + + {organizationProjects.map((item, i) => { + return ( + +
  • + {item.name} +
  • + + ); + })} + + + + + + ); + }} + +
    + ); +} diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 2b45b804d..c64c8e161 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "تعذر علينا تحديث عنوان المهمة.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "لا يمكن أن يتجاوز عنوان المهمة 255 حرفًا.", "TASK_IS_ALREADY_EPIC": "لا يمكن تغيير نوع المهمة الملحمية.", - "TASK_HAS_PARENT": "لا يمكن تغيير نوع المهمة حيث أن لديها بالفعل والد." + "TASK_HAS_PARENT": "لا يمكن تغيير نوع المهمة حيث أن لديها بالفعل والد.", + "PROJECT": "مشروع" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "أنت على وشك حذف الحالة {statusName} التي يتم استخدامها من قبل المهام النشطة؛ يرجى تأكيد الإجراء" diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 6a35a5b89..a504c9f88 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Не можахме да обновим заглавието на задачата.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Заглавието на задачата не може да надвишава 255 символа.", "TASK_IS_ALREADY_EPIC": "Епичният тип задача не може да бъде променен.", - "TASK_HAS_PARENT": "Типът задача не може да бъде променен, тъй като задачата вече има родител." + "TASK_HAS_PARENT": "Типът задача не може да бъде променен, тъй като задачата вече има родител.", + "PROJECT": "Проект" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Вие сте на път да изтриете състоянието {statusName}, което се използва от активни задачи; моля, потвърдете действието" diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index a8cd98b43..a29130ced 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Die Aufgabenbezeichnung konnte nicht aktualisiert werden.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Die Aufgabenbezeichnung darf nicht mehr als 255 Zeichen umfassen.", "TASK_IS_ALREADY_EPIC": "Epische Aufgabentypen können nicht geändert werden.", - "TASK_HAS_PARENT": "Der Aufgabentyp kann nicht geändert werden, da die Aufgabe bereits ein übergeordnetes Element hat." + "TASK_HAS_PARENT": "Der Aufgabentyp kann nicht geändert werden, da die Aufgabe bereits ein übergeordnetes Element hat.", + "PROJECT": "Projekt" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Sie sind dabei, den Status {statusName} zu löschen, der von aktiven Aufgaben verwendet wird; bitte bestätigen Sie die Aktion" diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index e230c7a31..7ad4c6701 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "We couldn't update Task Title.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Task Title can't exceed 255 characters.", "TASK_IS_ALREADY_EPIC": "Epic Task Type can not be changed.", - "TASK_HAS_PARENT": "Task Type can not be changed as Task has already Parent." + "TASK_HAS_PARENT": "Task Type can not be changed as Task has already Parent.", + "PROJECT": "Project" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "You are about to Delete the Status {statusName} that is used by active tasks; please confirm action" diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 7e8997c94..072a30752 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "No pudimos actualizar el título de la tarea.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "El título de la tarea no puede exceder 255 caracteres.", "TASK_IS_ALREADY_EPIC": "La tarea de tipo Épico no se puede cambiar.", - "TASK_HAS_PARENT": "El tipo de tarea no se puede cambiar porque la tarea ya tiene un padre." + "TASK_HAS_PARENT": "El tipo de tarea no se puede cambiar porque la tarea ya tiene un padre.", + "PROJECT": "Proyecto" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Está a punto de eliminar el estado {statusName} que está siendo utilizado por tareas activas; por favor confirme la acción" diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 2c70c0917..03e6d72ae 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Impossible de mettre à jour le titre de la tâche.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Le titre de la tâche ne peut pas dépasser 255 caractères.", "TASK_IS_ALREADY_EPIC": "Le type de tâche épique ne peut pas être modifié.", - "TASK_HAS_PARENT": "Le type de tâche ne peut pas être modifié car la tâche a déjà un parent." + "TASK_HAS_PARENT": "Le type de tâche ne peut pas être modifié car la tâche a déjà un parent.", + "PROJECT": "Projet" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Vous êtes sur le point de supprimer le statut {statusName} qui est utilisé par des tâches actives ; veuillez confirmer l'action" diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index e6c81b8fc..009eaf700 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "לא יכולנו לעדכן את כותרת המשימה.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "כותרת משימה לא יכולה לעלות על 255 תווים.", "TASK_IS_ALREADY_EPIC": "לא ניתן לשנות סוג משימה אפית.", - "TASK_HAS_PARENT": "לא ניתן לשנות סוג משימה מכיוון שלמשימה כבר יש הורה." + "TASK_HAS_PARENT": "לא ניתן לשנות סוג משימה מכיוון שלמשימה כבר יש הורה.", + "PROJECT": "פרויקט" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "אתה עומד למחוק את המצב {statusName} המשמש במשימות פעילות; נא לאשר פעולה" diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 945980bd8..c9810b03a 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Non è stato possibile aggiornare il Titolo del Compito.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Il Titolo del Compito non può superare i 255 caratteri.", "TASK_IS_ALREADY_EPIC": "Il tipo di Compito Epico non può essere cambiato.", - "TASK_HAS_PARENT": "Il tipo di Compito non può essere cambiato poiché il Compito ha già un Genitore." + "TASK_HAS_PARENT": "Il tipo di Compito non può essere cambiato poiché il Compito ha già un Genitore.", + "PROJECT": "Progetto" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Stai per eliminare lo stato {statusName} utilizzato da attività attive; per favore conferma l'azione" diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 2445311c8..15022dd3d 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "We konden de taaktitel niet bijwerken.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "De taaktitel kan niet langer zijn dan 255 tekens.", "TASK_IS_ALREADY_EPIC": "Epic taaktype kan niet worden gewijzigd.", - "TASK_HAS_PARENT": "Taaktype kan niet worden gewijzigd omdat de taak al een bovenliggende taak heeft." + "TASK_HAS_PARENT": "Taaktype kan niet worden gewijzigd omdat de taak al een bovenliggende taak heeft.", + "PROJECT": "Project" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "U staat op het punt de status {statusName} te verwijderen die wordt gebruikt door actieve taken; bevestig alstublieft de actie" diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 198f39004..3ff42ee43 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Nie udało się zaktualizować Tytułu Zadania.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Tytuł Zadania nie może przekraczać 255 znaków.", "TASK_IS_ALREADY_EPIC": "Typ Zadania jako 'Epic' nie może zostać zmieniony.", - "TASK_HAS_PARENT": "Typ Zadania nie może zostać zmieniony, ponieważ Zadanie ma już przypisanego rodzica." + "TASK_HAS_PARENT": "Typ Zadania nie może zostać zmieniony, ponieważ Zadanie ma już przypisanego rodzica.", + "PROJECT": "Projekt" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Zaraz usuniesz status {statusName}, który jest używany przez aktywne zadania; proszę potwierdzić akcję" diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 033cc1a79..4ae42a04d 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Não foi possível atualizar o Título da Tarefa.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "O Título da Tarefa não pode exceder 255 caracteres.", "TASK_IS_ALREADY_EPIC": "O Tipo de Tarefa Épica não pode ser alterado.", - "TASK_HAS_PARENT": "O Tipo de Tarefa não pode ser alterado, pois a Tarefa já possui um Pai." + "TASK_HAS_PARENT": "O Tipo de Tarefa não pode ser alterado, pois a Tarefa já possui um Pai.", + "PROJECT": "Projeto" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Está prestes a eliminar o status {statusName} que está a ser usado por tarefas ativas; por favor, confirme a ação" diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 3ab812538..a64a38134 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "Мы не смогли обновить заголовок задачи.", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "Заголовок задачи не может превышать 255 символов.", "TASK_IS_ALREADY_EPIC": "Тип задачи 'Epic' не может быть изменен.", - "TASK_HAS_PARENT": "Тип задачи не может быть изменен, так как у задачи уже есть родитель." + "TASK_HAS_PARENT": "Тип задачи не может быть изменен, так как у задачи уже есть родитель.", + "PROJECT": "Проект" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "Вы собираетесь удалить статус {statusName}, который используется активными задачами; пожалуйста, подтвердите действие" diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 50d39d2d2..311077623 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -343,7 +343,8 @@ "TASK_TITLE_CHARACTER_LIMIT_ERROR_TITLE": "无法更新任务标题。", "TASK_TITLE_CHARACTER_LIMIT_ERROR_DESCRIPTION": "任务标题不能超过255个字符。", "TASK_IS_ALREADY_EPIC": "史诗类型任务无法更改。", - "TASK_HAS_PARENT": "任务已有父任务,无法更改任务类型。" + "TASK_HAS_PARENT": "任务已有父任务,无法更改任务类型。", + "PROJECT": "项目" }, "taskStatus": { "DELETE_STATUS_CONFIRMATION": "您即将删除正在被活跃任务使用的状态 {statusName};请确认操作" From bc6384930077a326301db21f9cd241d241a6728a Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Tue, 19 Nov 2024 22:35:23 +0200 Subject: [PATCH 21/41] [Feature] Task | Add project information in the task (#3350) * add project in task details page / possiblity to edit * set the project when creating task --- apps/web/app/hooks/features/useTaskInput.ts | 7 +- apps/web/app/hooks/features/useTeamTasks.ts | 16 +- .../blocks/task-secondary-info.tsx | 369 +++++++++++------- apps/web/components/ui/svgs/project-icon.tsx | 18 + .../web/lib/features/auth-user-task-input.tsx | 18 +- apps/web/lib/features/task/task-input.tsx | 18 +- 6 files changed, 291 insertions(+), 155 deletions(-) create mode 100644 apps/web/components/ui/svgs/project-icon.tsx diff --git a/apps/web/app/hooks/features/useTaskInput.ts b/apps/web/app/hooks/features/useTaskInput.ts index fe21f5716..d5d803128 100644 --- a/apps/web/app/hooks/features/useTaskInput.ts +++ b/apps/web/app/hooks/features/useTaskInput.ts @@ -63,6 +63,7 @@ export function useTaskInput({ const taskSize = useRef(null); const taskDescription = useRef(null); const taskLabels = useRef<[] | ITaskLabelsItemList[]>([]); + const taskProject = useRef(null); const tasks = customTasks || teamTasks; @@ -160,7 +161,8 @@ export function useTaskInput({ priority: taskPriority.current || undefined, size: taskSize.current || undefined, tags: taskLabels.current || [], - description: taskDescription.current + description: taskDescription.current, + projectId : taskProject.current }, !autoAssignTaskAuth ? assignToUsers : undefined ).then((res) => { @@ -233,7 +235,8 @@ export function useTaskInput({ taskLabels, taskDescription, user, - userRef + userRef, + taskProject, }; } diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index a85217c6c..8b1fa8e9f 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -296,7 +296,8 @@ export function useTeamTasks() { priority, size, tags, - description + description, + projectId }: { taskName: string; issueType?: string; @@ -306,6 +307,7 @@ export function useTeamTasks() { size?: string; tags?: ITaskLabelsItemList[]; description?: string | null; + projectId?: string | null; }, members?: { id: string }[] ) => { @@ -319,11 +321,13 @@ export function useTeamTasks() { tags, // Set Project Id to cookie // TODO: Make it dynamic when we add Dropdown in Navbar - ...(activeTeam?.projects && activeTeam?.projects.length > 0 - ? { - projectId: activeTeam.projects[0].id - } - : {}), + + // ...(activeTeam?.projects && activeTeam?.projects.length > 0 + // ? { + // projectId: activeTeam.projects[0].id + // } + // : {}), + projectId, ...(description ? { description: `

    ${description}

    ` } : {}), ...(members ? { members } : {}), taskStatusId: taskStatusId diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx index 88c8690fd..959b2c4da 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx @@ -4,20 +4,16 @@ import { detailedTaskState } from '@app/stores'; import { PlusIcon } from '@heroicons/react/20/solid'; import { Button, Card, Modal, SpinnerLoader, Tooltip } from 'lib/components'; import { - ActiveTaskPropertiesDropdown, - ActiveTaskSizesDropdown, - ActiveTaskStatusDropdown, - ActiveTaskVersionDropdown, - EpicPropertiesDropdown as TaskEpicDropdown, - TaskLabels, - TaskStatus, - useTaskLabelsValue + ActiveTaskPropertiesDropdown, + ActiveTaskSizesDropdown, + ActiveTaskStatusDropdown, + ActiveTaskVersionDropdown, + EpicPropertiesDropdown as TaskEpicDropdown, + TaskLabels, + TaskStatus, + useTaskLabelsValue } from 'lib/features'; -import { - TaskPrioritiesForm, - TaskSizesForm, - TaskStatusesForm -} from 'lib/settings'; +import { TaskPrioritiesForm, TaskSizesForm, TaskStatusesForm } from 'lib/settings'; import { VersionForm } from 'lib/settings/version-form'; import { cloneDeep } from 'lodash'; import Link from 'next/link'; @@ -29,69 +25,65 @@ import { ChevronDownIcon, Square4OutlineIcon } from 'assets/svg'; import { Listbox, Transition } from '@headlessui/react'; import { clsxm } from '@/app/utils'; import { organizationProjectsState } from '@/app/stores/organization-projects'; +import ProjectIcon from '@components/ui/svgs/project-icon'; type StatusType = 'version' | 'epic' | 'status' | 'label' | 'size' | 'priority'; const TaskSecondaryInfo = () => { - const task = useAtomValue(detailedTaskState); - const { updateTask } = useTeamTasks(); - - const { handleStatusUpdate } = useTeamTasks(); - - const t = useTranslations(); - - const modal = useModal(); - const [formTarget, setFormTarget] = useState(null); - - const openModalEditionHandle = useCallback( - (type: StatusType) => { - return () => { - setFormTarget(type); - modal.openModal(); - }; - }, - [modal] - ); - - const onVersionCreated = useCallback( - (version: ITaskVersionCreate) => { - handleStatusUpdate( - version.value || version.name, - 'version', - task?.taskStatusId, - task - ); - }, - [task, handleStatusUpdate] - ); - - const onTaskSelect = useCallback( - async (parentTask: ITeamTask | undefined) => { - if (!parentTask) return; - const childTask = cloneDeep(task); - - await updateTask({ - ...childTask, - parentId: parentTask.id ? parentTask.id : null, - parent: parentTask.id ? parentTask : null - } as any); - }, - [task, updateTask] - ); - - const taskLabels = useTaskLabelsValue(); - - const tags = useMemo(() => { - return ( - task?.tags - .map((tag) => { - return taskLabels[tag.name]; - }) - .filter(Boolean) || [] - ); - }, [taskLabels, task?.tags]); - - return ( + const task = useAtomValue(detailedTaskState); + const { updateTask } = useTeamTasks(); + + const { handleStatusUpdate } = useTeamTasks(); + + const t = useTranslations(); + + const modal = useModal(); + const [formTarget, setFormTarget] = useState(null); + + const openModalEditionHandle = useCallback( + (type: StatusType) => { + return () => { + setFormTarget(type); + modal.openModal(); + }; + }, + [modal] + ); + + const onVersionCreated = useCallback( + (version: ITaskVersionCreate) => { + handleStatusUpdate(version.value || version.name, 'version', task?.taskStatusId, task); + }, + [task, handleStatusUpdate] + ); + + const onTaskSelect = useCallback( + async (parentTask: ITeamTask | undefined) => { + if (!parentTask) return; + const childTask = cloneDeep(task); + + await updateTask({ + ...childTask, + parentId: parentTask.id ? parentTask.id : null, + parent: parentTask.id ? parentTask : null + } as any); + }, + [task, updateTask] + ); + + const taskLabels = useTaskLabelsValue(); + + const tags = useMemo(() => { + return ( + task?.tags + .map((tag) => { + return taskLabels[tag.name]; + }) + .filter(Boolean) || [] + ); + }, [taskLabels, task?.tags]); + + return (
    {/* Version */} @@ -112,6 +104,23 @@ const TaskSecondaryInfo = () => { + {/* Epic */} + {task && task.issueType === 'Story' && ( + + { + onTaskSelect({ + id: d + } as ITeamTask); + }} + className="lg:min-w-[170px] text-black" + forDetails={true} + sidebarUI={true} + taskStatusClassName="text-[0.625rem] w-[7.6875rem] h-[2.35rem] max-w-[7.6875rem] rounded 3xl:text-xs" + defaultValue={task.parentId || ''} + /> + + )} {/* Epic */} {task && task.issueType === 'Story' && ( @@ -130,8 +139,27 @@ const TaskSecondaryInfo = () => { )} + {task && } {task && } + {/* Task Status */} + + + + + {/* Task Status */} { + {/* Task Labels */} + + + + {tags.length > 0 && ( + +
    + {tags.map((tag, i) => { + return ( + + + + ); + })} +
    +
    + )} {/* Task Labels */} {
    - ); + ); }; const EpicParent = ({ task }: { task: ITeamTask }) => { - const t = useTranslations(); - - if (task?.issueType === 'Story') { - return <>; - } - - return (!task?.issueType || - task?.issueType === 'Task' || - task?.issueType === 'Bug') && - task?.rootEpic ? ( - - - -
    -
    - -
    -
    {`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`}
    -
    - -
    -
    - ) : ( - <> - ); + const t = useTranslations(); + + if (task?.issueType === 'Story') { + return <>; + } + + return (!task?.issueType || task?.issueType === 'Task' || task?.issueType === 'Bug') && task?.rootEpic ? ( + + + +
    +
    + +
    +
    {`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`}
    +
    + +
    +
    + ) : ( + <> + ); }; - - interface ITaskProjectDropdownProps { - task: ITeamTask; + task?: ITeamTask; + controlled?: boolean; + onChange?: (project: IProject) => void; + styles?: { + container?: string; // The dropdown element + value?: string; + listCard?: string; // The listbox + }; } export default TaskSecondaryInfo; @@ -282,50 +337,58 @@ export default TaskSecondaryInfo; * * @param {Object} props - The props object * @param {ITeamTask} props.task - The ITeamTask object which + * @param {boolean} props.controlled - If [true], changes are managed by external handlers (i.e :props.onChange) + * @param {(project: IProject) => void} props.onChange - The function called when user selects a value (external handler) * * @returns {JSX.Element} - The Dropdown element */ -function ProjectDropDown (props : ITaskProjectDropdownProps) { - - const {task} = props +export function ProjectDropDown(props: ITaskProjectDropdownProps) { + const { task, controlled = false, onChange, styles } = props; - const organizationProjects = useAtomValue(organizationProjectsState) - const {getOrganizationProjects} = useOrganizationProjects() - const {updateTask, updateLoading} = useTeamTasks() - const t = useTranslations() + const organizationProjects = useAtomValue(organizationProjectsState); + const { getOrganizationProjects } = useOrganizationProjects(); + const { updateTask, updateLoading } = useTeamTasks(); + const t = useTranslations(); useEffect(() => { - getOrganizationProjects() - },[getOrganizationProjects]) - + getOrganizationProjects(); + }, [getOrganizationProjects]); const [selected, setSelected] = useState(); // Set the task project if any useEffect(() => { - setSelected(organizationProjects.find(project => { - return project.id === task.projectId - })) - },[organizationProjects, task.projectId]) - - // Update the project - const handleUpdateProject = useCallback(async (project : IProject) => { - try { - await updateTask({ ...task, projectId: project.id }); - - setSelected(project); - } catch (error) { - console.error(error); + if (task) { + setSelected( + organizationProjects.find((project) => { + return project.id == task.projectId; + }) + ); } - },[task, updateTask]) + }, [organizationProjects, task, task?.projectId]); + // Update the project + const handleUpdateProject = useCallback( + async (project: IProject) => { + try { + if (task) { + await updateTask({ ...task, projectId: project.id }); + } + } catch (error) { + console.error(error); + } + }, + [task, updateTask] + ); // Remove the project const handleRemoveProject = useCallback(async () => { try { - await updateTask({ ...task, projectId: null }); + if (task) { + await updateTask({ ...task, projectId: null }); - setSelected(undefined); + setSelected(undefined); + } } catch (error) { console.error(error); } @@ -334,25 +397,42 @@ function ProjectDropDown (props : ITaskProjectDropdownProps) { return (
    { + if (controlled && onChange) { + onChange(project); + } else { + handleUpdateProject(project); + } + + setSelected(project); + }} > {({ open }) => { return ( <> + {selected && ( +
    + +
    + )} {updateLoading ? ( ) : ( -

    {selected?.name ?? 'Project'}

    +

    + {selected?.name ?? 'Project'} +

    )} {organizationProjects.map((item, i) => { return ( @@ -386,13 +469,15 @@ function ProjectDropDown (props : ITaskProjectDropdownProps) { ); })} - + {!controlled && ( + + )} diff --git a/apps/web/components/ui/svgs/project-icon.tsx b/apps/web/components/ui/svgs/project-icon.tsx new file mode 100644 index 000000000..e15ec9d40 --- /dev/null +++ b/apps/web/components/ui/svgs/project-icon.tsx @@ -0,0 +1,18 @@ +export default function ProjectIcon() { + return ( + + + + + + ); +} diff --git a/apps/web/lib/features/auth-user-task-input.tsx b/apps/web/lib/features/auth-user-task-input.tsx index 33a6452ad..949392eac 100644 --- a/apps/web/lib/features/auth-user-task-input.tsx +++ b/apps/web/lib/features/auth-user-task-input.tsx @@ -9,6 +9,7 @@ import { TaskInput } from './task/task-input'; import { TaskLabels } from './task/task-labels'; import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown, ActiveTaskStatusDropdown } from './task/task-status'; import { useTranslations } from 'next-intl'; +import { ProjectDropDown } from '@components/pages/task/details-section/blocks/task-secondary-info'; export function AuthUserTaskInput({ className }: IClassName) { const t = useTranslations(); @@ -31,29 +32,38 @@ export function AuthUserTaskInput({ className }: IClassName) {
    + {activeTeamTask && ( + + )}
    {/*
    diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx index 1ae8b0945..31d551d49 100644 --- a/apps/web/lib/features/task/task-input.tsx +++ b/apps/web/lib/features/task/task-input.tsx @@ -58,6 +58,7 @@ import { useTranslations } from 'next-intl'; import { useInfinityScrolling } from '@app/hooks/useInfinityFetch'; import { ObserverComponent } from '@components/shared/Observer'; import { LazyRender } from 'lib/components/lazy-render'; +import { ProjectDropDown } from '@components/pages/task/details-section/blocks/task-secondary-info'; type Props = { task?: Nullable; @@ -540,7 +541,8 @@ function TaskCard({ taskPriority, taskSize, taskLabels, - taskDescription + taskDescription, + taskProject } = datas; const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5); @@ -640,6 +642,20 @@ function TaskCard({ }} task={datas.inputTask} /> + + { + if (taskProject) { + taskProject.current = project.id + } + }} + /> +
    )} From 9ab591b29daf9a697c8c5441370dbecfe1372bb4 Mon Sep 17 00:00:00 2001 From: Arick Bulakali <85836702+NdekoCode@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:42:59 +0200 Subject: [PATCH 22/41] Fix/layout size and scroll effect (#3351) * refactor(web): [Layout] add app container component * refactor(web): [Header] make the global header fixed on scroll * refactor(web): [Footer] make the global footer fixed dynamically on scroll and setup on the layout * refactor(web): [Layout] update specific position of elements using z-index * fix(web): layout size * refactor(web): [Layout] update layout height * refactor(web): [UserTeamCard] fix size on resizing and w-max * fix(web): add scroll effect on team-members-card-view * refactor(web): [Layout] add resize component to global layout * refactor(web): fix body bg in light mode, make main header fixed on tasks pages and profile page * fix(web): [Setting] setting page layout * fix(web): [Eslint] error and add an environnement variable for checking build without eslint errors * refactor(web): fixed task detail header * docs(web): add main-layout docs * docs(web): add main-layout docs * refactor(web): fix my-tasks page * fix(web): [Profile] fix outstanding task overflow * fix(web): fix footer fixed on timesheet, settings and kanban pages * refactor(web): [Kanban] add a dynamic height on kanban item * fix(web): [Kanban] fix kanban overscroll * fix(web): [Dashboard] fix breadcum size * fix(web): [Team's Tasks'] fix space on team's tasks page * refactor(web): [Profile] optimize space on profile page * refactor(web): [Timesheet] optimize spaces on reports/timesheet page * fix(web): [Setting] double scroll on setting page * test: build --- apps/web/.env | 4 + apps/web/.env.sample | 4 + apps/web/app/[locale]/kanban/page.tsx | 525 ++++---- apps/web/app/[locale]/layout.tsx | 6 +- apps/web/app/[locale]/page-component.tsx | 91 +- .../app/[locale]/profile/[memberId]/page.tsx | 131 +- apps/web/app/[locale]/settings/layout.tsx | 90 +- .../app/[locale]/settings/personal/page.tsx | 7 +- apps/web/app/[locale]/task/[id]/component.tsx | 4 +- apps/web/app/[locale]/task/[id]/page.tsx | 122 +- apps/web/app/[locale]/team/tasks/page.tsx | 127 +- .../[memberId]/components/EditTaskModal.tsx | 428 +++---- .../components/FilterWithStatus.tsx | 82 +- .../components/RejectSelectedModal.tsx | 167 +-- .../components/TimeSheetFilterPopover.tsx | 278 +++-- .../components/TimesheetFilterDate.tsx | 616 +++++----- .../[memberId]/components/TimesheetView.tsx | 36 +- .../[locale]/timesheet/[memberId]/page.tsx | 365 +++--- apps/web/app/hooks/features/useKanban.ts | 263 ++-- apps/web/app/layout.tsx | 2 +- .../components/pages/team/tasks/TaskTable.tsx | 26 +- .../pages/team/tasks/tasks-data-table.tsx | 204 +--- apps/web/components/ui/data-table.tsx | 19 +- apps/web/components/ui/scroll-area.tsx | 72 +- apps/web/lib/components/Kanban.tsx | 81 +- apps/web/lib/components/accordian.tsx | 2 +- apps/web/lib/components/container.tsx | 2 +- apps/web/lib/components/sidebar-accordian.tsx | 4 +- .../features/all-teams-members-card-view.tsx | 6 +- .../users-teams-card/member-infos.tsx | 5 +- .../all-teams/users-teams-card/user-card.tsx | 35 +- .../user-team-active-task-times.tsx | 8 +- .../user-team-active-task.tsx | 10 +- .../user-team-task-estimate.tsx | 8 +- .../user-team-today-worked.tsx | 13 +- .../integrations/activity-calendar/index.tsx | 253 ++-- .../calendar/table-time-sheet.tsx | 1064 ++++++++--------- .../features/task/daily-plan/future-tasks.tsx | 12 +- .../task/daily-plan/outstanding-all.tsx | 3 +- .../task/daily-plan/outstanding-date.tsx | 5 +- .../features/task/daily-plan/past-tasks.tsx | 1 + apps/web/lib/features/task/task-card.tsx | 58 +- apps/web/lib/features/task/task-filters.tsx | 18 +- apps/web/lib/features/task/task-status.tsx | 4 +- apps/web/lib/features/team-member-cell.tsx | 152 ++- apps/web/lib/features/team-member-header.tsx | 42 +- .../lib/features/team-members-card-view.tsx | 3 +- .../lib/features/team-members-kanban-view.tsx | 554 ++++----- .../lib/features/team-members-table-view.tsx | 1 + .../user-team-block-header.tsx | 408 +++---- .../features/team/user-team-card/index.tsx | 2 +- .../team/user-team-card/task-skeleton.tsx | 25 +- .../user-team-table-header.tsx | 3 +- apps/web/lib/features/unverified-email.tsx | 2 +- apps/web/lib/features/user-nav-menu.tsx | 8 +- apps/web/lib/features/user-profile-plans.tsx | 16 +- apps/web/lib/features/user-profile-tasks.tsx | 4 +- apps/web/lib/layout/AppContainer.tsx | 42 + apps/web/lib/layout/GlobalFooter.tsx | 42 + apps/web/lib/layout/GlobalHeader.tsx | 68 ++ apps/web/lib/layout/footer.tsx | 59 +- apps/web/lib/layout/main-layout.tsx | 205 ++-- apps/web/lib/utils.ts | 26 + apps/web/next.config.js | 11 +- apps/web/package.json | 2 +- apps/web/styles/globals.css | 98 +- yarn.lock | 34 +- 67 files changed, 3630 insertions(+), 3438 deletions(-) create mode 100644 apps/web/lib/layout/AppContainer.tsx create mode 100644 apps/web/lib/layout/GlobalFooter.tsx create mode 100644 apps/web/lib/layout/GlobalHeader.tsx diff --git a/apps/web/.env b/apps/web/.env index 41c6a1aa3..9180c30c7 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -133,3 +133,7 @@ NEXT_PUBLIC_CHATWOOT_API_KEY= # PostHog NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + +# Warning: IF TRUE This allows production builds to successfully complete even if +# your project has ESLint errors. +NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true diff --git a/apps/web/.env.sample b/apps/web/.env.sample index 7a6cedce2..62ae03258 100644 --- a/apps/web/.env.sample +++ b/apps/web/.env.sample @@ -83,3 +83,7 @@ NEXT_PUBLIC_MEET_DOMAIN="meet.ever.team" # Private Variables (Meet) MEET_JWT_APP_ID=ever_teams MEET_JWT_APP_SECRET= + +# Warning: IF TRUE This allows production builds to successfully complete even if +# your project has ESLint errors. +NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true diff --git a/apps/web/app/[locale]/kanban/page.tsx b/apps/web/app/[locale]/kanban/page.tsx index 71584b962..018561643 100644 --- a/apps/web/app/[locale]/kanban/page.tsx +++ b/apps/web/app/[locale]/kanban/page.tsx @@ -1,23 +1,16 @@ 'use client'; - import { KanbanTabs } from '@app/constants'; -import { - useAuthenticateUser, - useModal, - useOrganizationTeams -} from '@app/hooks'; +import { useAuthenticateUser, useModal, useOrganizationTeams } from '@app/hooks'; import { useKanban } from '@app/hooks/features/useKanban'; import KanbanBoardSkeleton from '@components/shared/skeleton/KanbanBoardSkeleton'; import { withAuthentication } from 'lib/app/authenticator'; -import { Breadcrumb, Container, Divider } from 'lib/components'; +import { Breadcrumb, Container } from 'lib/components'; import { KanbanView } from 'lib/features/team-members-kanban-view'; -import { Footer, MainLayout } from 'lib/layout'; +import { MainLayout } from 'lib/layout'; import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useParams, useSearchParams } from 'next/navigation'; -import ImageComponent, { - ImageOverlapperProps -} from 'lib/components/image-overlapper'; +import ImageComponent, { ImageOverlapperProps } from 'lib/components/image-overlapper'; import Separator from '@components/ui/separator'; import HeaderTabs from '@components/pages/main/header-tabs'; import { AddIcon, PeoplesIcon } from 'assets/svg'; @@ -25,288 +18,262 @@ import { InviteFormModal } from 'lib/features/team/invite/invite-form-modal'; import { userTimezone } from '@app/helpers'; import KanbanSearch from '@components/pages/kanban/search-bar'; import { - EpicPropertiesDropdown, - StatusDropdown, - TStatusItem, - TaskLabelsDropdown, - TaskPropertiesDropdown, - TaskSizesDropdown, - taskIssues, - useStatusValue + EpicPropertiesDropdown, + StatusDropdown, + TStatusItem, + TaskLabelsDropdown, + TaskPropertiesDropdown, + TaskSizesDropdown, + taskIssues, + useStatusValue } from 'lib/features'; import { useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; import { CircleIcon } from 'lucide-react'; import { XMarkIcon } from '@heroicons/react/20/solid'; -import Head from 'next/head'; -import { clsxm } from '@app/utils'; const Kanban = () => { - const { - data, - setSearchTasks, - searchTasks, - isLoading, - setPriority, - setSizes, - setLabels, - setEpics, - setIssues, - issues - } = useKanban(); + const { + data, + setSearchTasks, + searchTasks, + isLoading, + setPriority, + setSizes, + setLabels, + setEpics, + setIssues, + issues + } = useKanban(); + + const { activeTeam, isTrackingEnabled } = useOrganizationTeams(); + const t = useTranslations(); + const params = useParams<{ locale: string }>(); + const fullWidth = useAtomValue(fullWidthState); + const currentLocale = params ? params.locale : null; + const [activeTab, setActiveTab] = useState(KanbanTabs.TODAY); + const employee = useSearchParams().get('employee'); + const breadcrumbPath = useMemo( + () => [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: t('common.KANBAN'), href: `/${currentLocale}/kanban` } + ], + [activeTeam?.name, currentLocale, t] + ); - const { activeTeam, isTrackingEnabled } = useOrganizationTeams(); - const t = useTranslations(); - const params = useParams<{ locale: string }>(); - const fullWidth = useAtomValue(fullWidthState); - const currentLocale = params ? params.locale : null; - const [activeTab, setActiveTab] = useState(KanbanTabs.TODAY); - const employee = useSearchParams().get('employee'); - const breadcrumbPath = useMemo( - () => [ - { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, - { title: activeTeam?.name || '', href: '/' }, - { title: t('common.KANBAN'), href: `/${currentLocale}/kanban` } - ], - [activeTeam?.name, currentLocale, t] - ); + const activeTeamMembers = activeTeam?.members ? activeTeam.members : []; - const activeTeamMembers = activeTeam?.members ? activeTeam.members : []; + const teamMembers: ImageOverlapperProps[] = []; - const teamMembers: ImageOverlapperProps[] = []; + activeTeamMembers.map((member: any) => { + teamMembers.push({ + id: member.employee.user.id, + url: member.employee.user.imageUrl, + alt: member.employee.user.firstName + }); + }); + const tabs = [ + { name: t('common.TODAY'), value: KanbanTabs.TODAY }, + { name: t('common.YESTERDAY'), value: KanbanTabs.YESTERDAY }, + { name: t('common.TOMORROW'), value: KanbanTabs.TOMORROW } + ]; + const { user } = useAuthenticateUser(); + const { openModal, isOpen, closeModal } = useModal(); + const timezone = userTimezone(); - activeTeamMembers.map((member: any) => { - teamMembers.push({ - id: member.employee.user.id, - url: member.employee.user.imageUrl, - alt: member.employee.user.firstName - }); - }); - const tabs = [ - { name: t('common.TODAY'), value: KanbanTabs.TODAY }, - { name: t('common.YESTERDAY'), value: KanbanTabs.YESTERDAY }, - { name: t('common.TOMORROW'), value: KanbanTabs.TOMORROW } - ]; - const { user } = useAuthenticateUser(); - const { openModal, isOpen, closeModal } = useModal(); - const timezone = userTimezone(); - const { items } = useStatusValue<'issueType'>({ - status: taskIssues, - value: issues as any, - onValueChange: setIssues as any - }); + const { items } = useStatusValue<'issueType'>({ + status: taskIssues, + value: issues as any, + onValueChange: setIssues as any + }); - useEffect(() => { - const lastPath = breadcrumbPath.slice(-1)[0]; - if (employee) { - if (lastPath.title == 'Kanban') { - breadcrumbPath.push({ - title: employee, - href: `/${currentLocale}/kanban?employee=${employee}` - }); - } else { - breadcrumbPath.pop(); - breadcrumbPath.push({ - title: employee, - href: `/${currentLocale}/kanban?employee=${employee}` - }); - } - } else { - if (lastPath.title !== 'Kanban') { - breadcrumbPath.pop(); - } - } - }, [breadcrumbPath, currentLocale, employee]); - return ( - <> - - - {t('common.KANBAN')} {t('common.BOARD')} - - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    -
    -

    - {t('common.KANBAN')} {t('common.BOARD')} -

    -
    - - {`(`} - {timezone.split('(')[1]} - -
    - -
    - -
    - -
    + useEffect(() => { + const lastPath = breadcrumbPath.slice(-1)[0]; + if (employee) { + if (lastPath.title == 'Kanban') { + breadcrumbPath.push({ + title: employee, + href: `/${currentLocale}/kanban?employee=${employee}` + }); + } else { + breadcrumbPath.pop(); + breadcrumbPath.push({ + title: employee, + href: `/${currentLocale}/kanban?employee=${employee}` + }); + } + } else if (lastPath.title !== 'Kanban') { + breadcrumbPath.pop(); + } + }, [breadcrumbPath, currentLocale, employee]); + return ( + <> + + +
    +
    + + +
    +
    + +
    +
    +
    +

    + {t('common.KANBAN')} {t('common.BOARD')} +

    +
    + + {`(`} + {timezone.split('(')[1]} + +
    + +
    + +
    + +
    - -
    -
    -
    -
    - {tabs.map((tab) => ( -
    setActiveTab(tab.value)} - className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${ - activeTab === tab.value - ? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white' - : 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]' - }`} - style={{ - borderBottomWidth: '3px', - borderBottomStyle: 'solid' - }} - > - {tab.name} -
    - ))} -
    -
    -
    - setEpics(values || [])} - className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" - multiple={true} - /> -
    - {/*
    */} -
    -
    - -
    - {issues.icon ?? } -
    -

    {issues.name}

    -
    - {issues.value && ( -
    - setIssues({ - name: 'Issues', - icon: null, - bgColor: '', - value: '' - }) - } - className="w-5 h-5 z-50 p-0.5 cursor-pointer" - > - -
    - )} -
    + +
    +
    +
    +
    + {tabs.map((tab) => ( +
    setActiveTab(tab.value)} + className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${ + activeTab === tab.value + ? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white' + : 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]' + }`} + style={{ + borderBottomWidth: '3px', + borderBottomStyle: 'solid' + }} + > + {tab.name} +
    + ))} +
    +
    +
    + setEpics(values || [])} + className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" + multiple={true} + /> +
    + {/*
    */} +
    +
    + +
    + {issues.icon ?? } +
    +

    {issues.name}

    +
    + {issues.value && ( +
    + setIssues({ + name: 'Issues', + icon: null, + bgColor: '', + value: '' + }) + } + className="w-5 h-5 z-10 p-0.5 cursor-pointer" + > + +
    + )} +
    - { - setIssues(items.find((v) => v.name == e) as TStatusItem); - }} - issueType="issue" - /> -
    - {/*
    */} -
    - setLabels(values || [])} - className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" - multiple={true} - /> -
    -
    - setPriority(values || [])} - className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" - multiple={true} - /> -
    -
    - setSizes(values || [])} - className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" - multiple={true} - /> -
    -
    - -
    - -
    -
    - {/*
    */} - -
    -
    - {/** TODO:fetch teamtask based on days */} - {activeTab && ( // add filter for today, yesterday and tomorrow -
    - {Object.keys(data).length > 0 ? ( - - ) : ( - - )} -
    - )} -
    - -
    - -
    -
    - - - ); + { + setIssues(items.find((v) => v.name == e) as TStatusItem); + }} + issueType="issue" + /> +
    + {/*
    */} +
    + setLabels(values || [])} + className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" + multiple={true} + /> +
    +
    + setPriority(values || [])} + className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" + multiple={true} + /> +
    +
    + setSizes(values || [])} + className="lg:min-w-[140px] pt-[3px] mt-4 mb-2 lg:mt-0" + multiple={true} + /> +
    +
    + +
    + +
    +
    + {/*
    */} + +
    + } + > + {/** TODO:fetch teamtask based on days */} +
    + {activeTab && + (Object.keys(data).length > 0 ? ( + + ) : ( + // add filter for today, yesterday and tomorrow +
    + +
    + ))} +
    + + + + ); }; export default withAuthentication(Kanban, { displayName: 'Kanban' }); diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index 8efdc3b8e..8dd5ce0ce 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -130,7 +130,11 @@ const LocaleLayout = ({ children, params: { locale }, pageProps }: PropsWithChil */} - + diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index febc94842..68f697eed 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -1,7 +1,6 @@ /* eslint-disable no-mixed-spaces-and-tabs */ 'use client'; - import React, { useEffect, useState } from 'react'; import { useOrganizationTeams } from '@app/hooks'; import { clsxm } from '@app/utils'; @@ -29,12 +28,11 @@ import { headerTabs } from '@app/stores/header-tabs'; import { usePathname } from 'next/navigation'; import { PeoplesIcon } from 'assets/svg'; import TeamMemberHeader from 'lib/features/team-member-header'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; import { TeamOutstandingNotifications } from 'lib/features/team/team-outstanding-notifications'; function MainPage() { const t = useTranslations(); - const [headerSize, setHeaderSize] = useState(10); + const [headerSize] = useState(10); const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams(); const [fullWidth, setFullWidth] = useAtom(fullWidthState); const [view, setView] = useAtom(headerTabs); @@ -67,63 +65,40 @@ function MainPage() { - - -
    - - {/* */} - - setHeaderSize(size)} - > -
    -
    -
    -
    - - - -
    - -
    - -
    -
    + mainHeaderSlot={ +
    +
    +
    +
    + + + +
    -
    -
    - +
    + +
    +
    - +
    +
    + - -
    + - {isTeamMember ? ( - - ) : null} -
    - +
    -
    - - - - {/* */} - - {isTeamMember ? : } - - -
    + {isTeamMember ? : null} +
    + +
    +
    + } + footerClassName={clsxm('')} + > + +
    {isTeamMember ? : }
    @@ -131,19 +106,19 @@ function MainPage() { ); } -function TaskTimerSection({ isTrackingEnabled }: { isTrackingEnabled: boolean }) { +function TaskTimerSection({ isTrackingEnabled }: Readonly<{ isTrackingEnabled: boolean }>) { const [showInput, setShowInput] = React.useState(false); return (
    {isTrackingEnabled ? ( -
    +
    ) : null} diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index c3d41d65c..690ee558d 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -17,7 +17,6 @@ import { ScreenshootTab } from 'lib/features/activity/screenshoots'; import { AppsTab } from 'lib/features/activity/apps'; import { VisitedSitesTab } from 'lib/features/activity/visited-sites'; import { activityTypeState } from '@app/stores/activity-type'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; import { UserProfileDetail } from './components/UserProfileDetail'; import { cn } from 'lib/utils'; // import { ActivityCalendar } from 'lib/features/activity/calendar'; @@ -26,7 +25,7 @@ export type FilterTab = 'Tasks' | 'Screenshots' | 'Apps' | 'Visited Sites'; const Profile = React.memo(function ProfilePage({ params }: { params: { memberId: string } }) { const profile = useUserProfilePage(); - const [headerSize, setHeaderSize] = useState(10); + const [headerSize] = useState(10); const { user } = useAuthenticateUser(); const { isTrackingEnabled, activeTeam, activeTeamManagers } = useOrganizationTeams(); const members = activeTeam?.members; @@ -115,79 +114,71 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId } return ( - - - setHeaderSize(size)} - > - -
    - {/* Breadcrumb */} -
    - - - - - -
    + +
    + {/* Breadcrumb */} +
    + + + + + +
    - {/* User Profile Detail */} -
    - - - {profileIsAuthUser && isTrackingEnabled && ( - - )} -
    - {/* TaskFilter */} - + {/* User Profile Detail */} +
    + + + {profileIsAuthUser && isTrackingEnabled && ( + + )}
    - - {/*
    + {/* TaskFilter */} + +
    + + } + > + {/*
    */} - - - - {hook.tab == 'worked' && canSeeActivity && ( - -
    - {Object.keys(activityScreens).map((filter, i) => ( -
    - {i !== 0 && } -
    changeActivityFilter(filter as FilterTab)} - > - {filter} -
    -
    - ))} + {hook.tab == 'worked' && canSeeActivity && ( + +
    + {Object.keys(activityScreens).map((filter, i) => ( +
    + {i !== 0 && } +
    changeActivityFilter(filter as FilterTab)} + > + {filter} +
    - - )} - - - {hook.tab !== 'worked' || activityFilter == 'Tasks' ? ( - - ) : ( - activityScreens[activityFilter] ?? null - )} - - - + ))} +
    +
    + )} + + + {hook.tab !== 'worked' || activityFilter == 'Tasks' ? ( + + ) : ( + activityScreens[activityFilter] ?? null + )} + ); }); diff --git a/apps/web/app/[locale]/settings/layout.tsx b/apps/web/app/[locale]/settings/layout.tsx index 232661884..7ef2e1df6 100644 --- a/apps/web/app/[locale]/settings/layout.tsx +++ b/apps/web/app/[locale]/settings/layout.tsx @@ -9,59 +9,57 @@ import { LeftSideSettingMenu } from 'lib/settings'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { useAtom, useAtomValue } from 'jotai'; -import { clsxm } from '@app/utils'; import { withAuthentication } from 'lib/app/authenticator'; import { usePathname } from 'next/navigation'; import { useOrganizationTeams } from '@app/hooks'; +import { cn } from '@/lib/utils'; const SettingsLayout = ({ children }: { children: JSX.Element }) => { - const { isTrackingEnabled } = useOrganizationTeams(); - const t = useTranslations(); - const [user] = useAtom(userState); - const fullWidth = useAtomValue(fullWidthState); - const pathName = usePathname(); - const getEndPath: any = pathName?.split('settings/')[1]; - const endWord: 'TEAM' | 'PERSONAL' = getEndPath?.toUpperCase(); - const breadcrumb = [ - { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, - { title: t('common.SETTINGS'), href: pathName as string }, - { title: t(`common.${endWord}`), href: pathName as string } - ]; + const { isTrackingEnabled } = useOrganizationTeams(); + const t = useTranslations(); + const [user] = useAtom(userState); + const fullWidth = useAtomValue(fullWidthState); + const pathName = usePathname(); + const getEndPath: any = pathName?.split('settings/')[1]; + const endWord: 'TEAM' | 'PERSONAL' = getEndPath?.toUpperCase(); + const breadcrumb = [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: t('common.SETTINGS'), href: pathName as string }, + { title: t(`common.${endWord}`), href: pathName as string } + ]; - if (!user) { - return ; - } else { - return ( - -
    - - - - + if (!user) { + return ; + } else { + return ( + + + + + - - -
    - -
    - -
    - {children} -
    -
    -
    -
    - ); - } + + +
    + } + > + +
    + +
    {children}
    +
    +
    + + ); + } }; export default withAuthentication(SettingsLayout, { displayName: 'Settings' }); diff --git a/apps/web/app/[locale]/settings/personal/page.tsx b/apps/web/app/[locale]/settings/personal/page.tsx index 4a2585354..63bb4889d 100644 --- a/apps/web/app/[locale]/settings/personal/page.tsx +++ b/apps/web/app/[locale]/settings/personal/page.tsx @@ -11,15 +11,15 @@ const Personal = () => { const t = useTranslations(); return ( -
    +
    - {/* @@ -38,7 +38,6 @@ const Personal = () => { diff --git a/apps/web/app/[locale]/task/[id]/component.tsx b/apps/web/app/[locale]/task/[id]/component.tsx index 4883d3600..46cea2009 100644 --- a/apps/web/app/[locale]/task/[id]/component.tsx +++ b/apps/web/app/[locale]/task/[id]/component.tsx @@ -22,9 +22,9 @@ interface ITaskDetailsComponentProps { export function TaskDetailsComponent(props: ITaskDetailsComponentProps) { const { task } = props; return ( -
    +
    -
    +
    diff --git a/apps/web/app/[locale]/task/[id]/page.tsx b/apps/web/app/[locale]/task/[id]/page.tsx index 44335da0e..2b0651f14 100644 --- a/apps/web/app/[locale]/task/[id]/page.tsx +++ b/apps/web/app/[locale]/task/[id]/page.tsx @@ -1,10 +1,6 @@ 'use client'; -import { - useOrganizationTeams, - useTeamTasks, - useUserProfilePage -} from '@app/hooks'; +import { useOrganizationTeams, useTeamTasks, useUserProfilePage } from '@app/hooks'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Container } from 'lib/components'; import { ArrowLeftIcon } from 'assets/svg'; @@ -18,72 +14,66 @@ import { useAtomValue } from 'jotai'; import { TaskDetailsComponent } from './component'; const TaskDetails = () => { - const profile = useUserProfilePage(); - const t = useTranslations(); - const router = useRouter(); - const params = useParams(); - const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); - const { - getTaskById, - detailedTask: task, - getTasksByIdLoading - } = useTeamTasks(); - const fullWidth = useAtomValue(fullWidthState); + const profile = useUserProfilePage(); + const t = useTranslations(); + const router = useRouter(); + const params = useParams(); + const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); + const { getTaskById, detailedTask: task, getTasksByIdLoading } = useTeamTasks(); + const fullWidth = useAtomValue(fullWidthState); - const id = params?.id; + const id = params?.id; - const breadcrumb = [ - { title: activeTeam?.name || '', href: '/' }, - { - title: JSON.parse(t('pages.taskDetails.BREADCRUMB')), - href: `/task/${id}` - } - ]; + const breadcrumb = [ + { title: activeTeam?.name || '', href: '/' }, + { + title: JSON.parse(t('pages.taskDetails.BREADCRUMB')), + href: `/task/${id}` + } + ]; - useEffect(() => { - if ( - router && - // If id is passed in query param - id && - // Either no task or task id doesn't match query id - (!task || (task && task.id !== id)) && - !getTasksByIdLoading - ) { - getTaskById(id as string); - } - }, [getTaskById, router, task, getTasksByIdLoading, id]); + useEffect(() => { + if ( + router && + // If id is passed in query param + id && + // Either no task or task id doesn't match query id + (!task || (task && task.id !== id)) && + !getTasksByIdLoading + ) { + getTaskById(id as string); + } + }, [getTaskById, router, task, getTasksByIdLoading, id]); - return ( - -
    - -
    - { - router.replace('/'); - }} - > - - + return ( + + +
    + { + router.replace('/'); + }} + > + + - -
    -
    -
    - - - {task && } - {/* */} - - - ); + +
    + +
    + } + > + + {task && } + {/* */} + + + ); }; export default withAuthentication(TaskDetails, { displayName: 'TaskDetails' }); diff --git a/apps/web/app/[locale]/team/tasks/page.tsx b/apps/web/app/[locale]/team/tasks/page.tsx index db1844d68..33cb32f28 100644 --- a/apps/web/app/[locale]/team/tasks/page.tsx +++ b/apps/web/app/[locale]/team/tasks/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Breadcrumb, Container } from 'lib/components'; +import { Breadcrumb, Container, Paginate } from 'lib/components'; import { MainLayout } from 'lib/layout'; import { useParams } from 'next/navigation'; import { useMemo } from 'react'; @@ -7,10 +7,21 @@ import { useTranslations } from 'next-intl'; import { useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; -import { TaskTable } from '@components/pages/team/tasks/TaskTable'; -import { useOrganizationTeams } from '@app/hooks'; +import { useOrganizationTeams, useTeamTasks } from '@app/hooks'; import { withAuthentication } from '@/lib/app/authenticator'; +import { ITeamTask } from '@/app/interfaces'; + +import { getCoreRowModel, getFilteredRowModel, useReactTable } from '@tanstack/react-table'; +import StatusBadge from '@components/pages/team/tasks/StatusBadge'; +import { getStatusColor } from '@/lib/utils'; +import FilterButton from '@components/pages/team/tasks/FilterButton'; +import { Input } from '@components/ui/input'; +import { Search } from 'lucide-react'; +import { Button } from '@components/ui/button'; +import { TaskTable } from '@components/pages/team/tasks/TaskTable'; +import { columns } from '@components/pages/team/tasks/columns'; +import { usePagination } from '@/app/hooks/features/usePagination'; const TeamTask = () => { const t = useTranslations(); const params = useParams<{ locale: string }>(); @@ -23,15 +34,113 @@ const TeamTask = () => { { title: activeTeam?.name || '', href: '/' }, { title: "Team's Task", href: `/${currentLocale}/team/task` } ], - [activeTeam?.name, currentLocale] + [activeTeam?.name, currentLocale, t] ); + const { tasks } = useTeamTasks(); + + const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } = + usePagination(tasks); + const table = useReactTable({ + data: currentItems, + columns, + + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel() + }); + return ( - - - - - + + + +
    +
    +

    + {t('sidebar.TEAMTASKS')} +

    + +
    +
    + + } + childrenClassName="bg-white dark:bg-dark--theme" + > +
    + + + +
    ); }; diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx index 3dffdc1b5..1828d9a9b 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx @@ -1,231 +1,231 @@ -import { Button, Modal, statusColor } from "@/lib/components"; -import { IoMdArrowDropdown } from "react-icons/io"; -import { FaRegClock } from "react-icons/fa"; -import { DatePickerFilter } from "./TimesheetFilterDate"; -import { useState } from "react"; -import { useTranslations } from "next-intl"; -import { clsxm } from "@/app/utils"; -import { Item, ManageOrMemberComponent, getNestedValue } from "@/lib/features/manual-time/manage-member-component"; -import { useTeamTasks } from "@/app/hooks"; -import { CustomSelect } from "@/lib/features"; -import { statusTable } from "./TimesheetAction"; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Button, Modal, statusColor } from '@/lib/components'; +import { IoMdArrowDropdown } from 'react-icons/io'; +import { FaRegClock } from 'react-icons/fa'; +import { DatePickerFilter } from './TimesheetFilterDate'; +import { useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { clsxm } from '@/app/utils'; +import { Item, ManageOrMemberComponent, getNestedValue } from '@/lib/features/manual-time/manage-member-component'; +import { useTeamTasks } from '@/app/hooks'; +import { CustomSelect } from '@/lib/features'; +import { statusTable } from './TimesheetAction'; export interface IEditTaskModalProps { - isOpen: boolean; - closeModal: () => void; - + isOpen: boolean; + closeModal: () => void; } export function EditTaskModal({ isOpen, closeModal }: IEditTaskModalProps) { - const { activeTeam } = useTeamTasks(); - const t = useTranslations(); - const [dateRange, setDateRange] = useState<{ from: Date | null }>({ - from: new Date(), - }); - const [endTime, setEndTime] = useState(''); - const [startTime, setStartTime] = useState(''); - const [isBillable, setIsBillable] = useState(false); - const [notes, setNotes] = useState(''); - const memberItemsLists = { - Project: activeTeam?.projects as [], - }; - const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { - // Handle value changes - }; - const selectedValues = { - Teams: null, - }; - - const handleChange = (field: string, selectedItem: Item | null) => { - // Handle field changes - }; - - const fields = [ - { - label: 'Project', - placeholder: 'Select a project', - isRequired: true, - valueKey: 'id', - displayKey: 'name', - element: 'Project' - }, - ]; - - const handleFromChange = (fromDate: Date | null) => { - setDateRange((prev) => ({ ...prev, from: fromDate })); - }; - return ( - -
    -
    - #321 Spike for creating calendar views on mobile -
    - for - Savannah Nguyen - -
    -
    -
    -
    - Task Time -
    - - 08:10h -
    -
    -
    -
    - - setStartTime(e.target.value)} - className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md" - required - /> -
    + const { activeTeam } = useTeamTasks(); + const t = useTranslations(); + const [dateRange, setDateRange] = useState<{ from: Date | null }>({ + from: new Date() + }); + const [endTime, setEndTime] = useState(''); + const [startTime, setStartTime] = useState(''); + const [isBillable, setIsBillable] = useState(false); + const [notes, setNotes] = useState(''); + const memberItemsLists = { + Project: activeTeam?.projects as [] + }; + const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { + // Handle value changes + }; + const selectedValues = { + Teams: null + }; -
    - + const handleChange = (field: string, selectedItem: Item | null) => { + // Handle field changes + }; - setEndTime(e.target.value)} - className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md" - required - /> -
    + const fields = [ + { + label: 'Project', + placeholder: 'Select a project', + isRequired: true, + valueKey: 'id', + displayKey: 'name', + element: 'Project' + } + ]; -
    -
    - {t("manualTime.DATE")} - -
    -
    - getNestedValue(item, displayKey) || ''} - itemToValue={(item, valueKey) => getNestedValue(item, valueKey) || ''} - /> -
    -
    - -
    - setIsBillable(!isBillable)} - label={t('pages.timesheet.BILLABLE.YES')} - /> - setIsBillable(!isBillable)} - label={t('pages.timesheet.BILLABLE.NO')} - /> -
    -
    -
    - Notes -