Skip to content

Commit

Permalink
feat: make AlertDialogConfirmation fully controllable via external props
Browse files Browse the repository at this point in the history
  • Loading branch information
Innocent-Akim committed Nov 16, 2024
1 parent 2936ab9 commit ec39ba9
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<StatusType, { icon: JSX.Element; title: string; action: StatusType }[]> = {
const buttonsConfig: Record<StatusType, { icon: JSX.Element; title: string; action: StatusAction }[]> = {
Pending: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" }
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
Approved: [
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" }
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
Rejected: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Pending" }
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
]
};

Expand Down
11 changes: 9 additions & 2 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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() {
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: ITimeSheet) => {
setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items])
}

return {
statusState,
Expand All @@ -20,6 +25,8 @@ export function useTimelogFilterOptions() {
setEmployeeState,
setProjectState,
setTaskState,
setStatusState
setStatusState,
handleSelectRowTimesheet,
selectTimesheet
};
}
47 changes: 23 additions & 24 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface GroupedTimesheet {
interface DeleteTimesheetParams {
organizationId: string;
tenantId: string;
logIds: string[];
logIds: ITimeSheet[];
}

const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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<void> => {
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]
);

Expand Down
8 changes: 4 additions & 4 deletions apps/web/app/services/client/api/timer/timer-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function deleteTaskTimesheetLogsApi({
}: {
organizationId: string,
tenantId: string,
logIds: string[]
logIds: ITimeSheet[]
}) {
// Validate required parameters
if (!organizationId || !tenantId || !logIds?.length) {
Expand All @@ -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()}`;
Expand Down
139 changes: 139 additions & 0 deletions apps/web/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"

const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"

const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
Loading

0 comments on commit ec39ba9

Please sign in to comment.