From ec39ba9344466fab09c4119b83f33d2011dd648a Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Sat, 16 Nov 2024 17:25:15 +0200 Subject: [PATCH] feat: make AlertDialogConfirmation fully controllable via external props --- .../[memberId]/components/TimesheetAction.tsx | 12 +- .../hooks/features/useTimelogFilterOptions.ts | 11 +- apps/web/app/hooks/features/useTimesheet.ts | 47 +++--- .../services/client/api/timer/timer-log.ts | 8 +- apps/web/components/ui/alert-dialog.tsx | 139 ++++++++++++++++++ apps/web/components/ui/button.tsx | 89 +++++------ .../components/alert-dialog-confirmation.tsx | 59 ++++++++ apps/web/lib/components/index.ts | 1 + .../calendar/table-time-sheet.tsx | 38 ++++- apps/web/lib/features/user-profile-plans.tsx | 5 +- apps/web/package.json | 9 +- yarn.lock | 14 +- 12 files changed, 340 insertions(+), 92 deletions(-) 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/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index 60733481f..6f0603867 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -1,4 +1,5 @@ -import { timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores'; +import { ITimeSheet } from '@/app/interfaces'; +import { timesheetDeleteState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores'; import { useAtom } from 'jotai'; export function useTimelogFilterOptions() { @@ -6,11 +7,15 @@ export function useTimelogFilterOptions() { 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: ITimeSheet) => { + setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items]) + } return { statusState, @@ -20,6 +25,8 @@ export function useTimelogFilterOptions() { setEmployeeState, setProjectState, setTaskState, - setStatusState + setStatusState, + handleSelectRowTimesheet, + selectTimesheet }; } diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index bb2db4ce8..73e079bb3 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -20,7 +20,7 @@ export interface GroupedTimesheet { interface DeleteTimesheetParams { organizationId: string; tenantId: string; - logIds: string[]; + logIds: ITimeSheet[]; } const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => { @@ -50,7 +50,7 @@ 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) @@ -84,35 +84,34 @@ export function useTimesheet({ ); - const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => { + + const handleDeleteTimesheet = (params: DeleteTimesheetParams) => { try { - return await queryDeleteTimesheet(params); + return queryDeleteTimesheet(params); } catch (error) { console.error('Error deleting timesheet:', error); throw error; } }; - const deleteTaskTimesheet = useCallback( - async ({ logIds }: DeleteTimesheetParams): Promise => { - 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; - } - }, + const deleteTaskTimesheet = useCallback(() => { + if (!user) { + throw new Error('User not authenticated'); + } + if (!logIds.length) { + throw new Error('No timesheet IDs provided for deletion'); + } + try { + handleDeleteTimesheet({ + organizationId: user.employee.organizationId, + tenantId: user.tenantId ?? "", + logIds + }); + } catch (error) { + console.error('Failed to delete timesheets:', error); + throw error; + } + }, [user, queryDeleteTimesheet] ); 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..8bab83002 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -75,7 +75,7 @@ export async function deleteTaskTimesheetLogsApi({ }: { organizationId: string, tenantId: string, - logIds: string[] + logIds: ITimeSheet[] }) { // Validate required parameters if (!organizationId || !tenantId || !logIds?.length) { @@ -91,11 +91,11 @@ export async function deleteTaskTimesheetLogsApi({ organizationId, tenantId }); - logIds.forEach((id, index) => { - if (!id) { + logIds.forEach((items, index) => { + if (!items) { throw new Error(`Invalid logId at index ${index}`); } - params.append(`logIds[${index}]`, id); + params.append(`logIds[${index}]`, items.id); }); const endPoint = `/timesheet/time-log?${params.toString()}`; 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..8cc5c8068 --- /dev/null +++ b/apps/web/lib/components/alert-dialog-confirmation.tsx @@ -0,0 +1,59 @@ +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 && ( + + )} + {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..12dfbcf6a 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,16 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { openModal, closeModal } = useModal(); + const { deleteTaskTimesheet, loadingDeleteTimesheet } = useTimesheet({}) + const { handleSelectRowTimesheet, selectTimesheet } = useTimelogFilterOptions() + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const handleConfirm = () => { + deleteTaskTimesheet(); + setIsDialogOpen(false); + }; + const handleCancel = () => { + setIsDialogOpen(false); + }; const t = useTranslations(); const [sorting, setSorting] = React.useState([]) @@ -209,7 +219,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,7 +227,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { case 'Rejected': openModal() break; - case 'Pending': + case 'Deleted': // TODO: Implement pending logic break; default: @@ -227,6 +237,17 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { return (
+ { // Pending implementation @@ -283,7 +304,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)} + checked={selectTimesheet.includes(task)} + />
{/* {/* 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==