Untitled
Take this noob daiimport { MoreVertical } from 'lucide-react'; import { useCallback, useMemo } from 'react'; import { AccessManager, APPROVAL_CONTROLLER_ROUTE, ApprovalListingType, ApprovalStatusTypeEnum, ApprovalStepTypeEnum, FetchData, FormatCurrency, FormatDisplayDate, GetObjectFromArray, GetUniqueObjectsFromArray, IndexOfObjectInArray, IsEmptyArray, IsUndefinedOrNull, ObjectDto, PRODUCT_IDENTIFIER, SortArrayObjectBy, toastBackendError, useApp, useGetEmployeeId, useUserHook, VerificationStatusTypeEnum, } from '@finnoto/core'; import { EmployeeAdvanceApprovalController } from '@finnoto/core/src/backend/ap/employee/controllers/employee.advance.approval.controller'; import { ExpenseApprovalController } from '@finnoto/core/src/backend/ap/expense/controllers/expense.approval.controller'; import { PurchaseOrderApprovalController } from '@finnoto/core/src/backend/ap/purchase/controllers/purchase.order.approval.controller'; import { PurchaseRequestApprovalController } from '@finnoto/core/src/backend/ap/purchase/controllers/purchase.request.approval.controller'; import { ProjectApprovalController } from '@finnoto/core/src/backend/ap/workflow/controllers'; import { Avatar, Button, cn, Collapse, ConfirmUtil, DropdownMenu, FormatDisplayDateStyled, handleActivityTimeline, Icon, NoDataFound, SlidingPane, Toast, } from '@finnoto/design-system'; import { EmployeeIconImage } from '@Components/BusinessImage/businessImage.component'; import { handleNavigationEmployeeDetail, openOverrideApproval, openTransferApproval, } from '@Utils/functions.utils'; import { ApprovalNonCheckedSvgIcon, VerifiedTickSvgIcon } from 'assets'; interface InvoiceApprovalWorkflowProps { data: ObjectDto; workflows: ObjectDto[]; currentActiveWorkflow?: ObjectDto; isAnyRejected?: boolean; canApproveInvoice?: boolean; active_approval?: ObjectDto; type?: ApprovalListingType; openEdit?: () => void; callback?: () => void; } const InvoiceApprovalWorkflow = ({ data, workflows, currentActiveWorkflow, isAnyRejected, canApproveInvoice, active_approval, type = 'expense', openEdit = () => {}, callback = () => {}, }: InvoiceApprovalWorkflowProps) => { const { id: document_id, party_status_id } = data || {}; const { user } = useUserHook(); const isCreatorIsCurrentUser = useMemo(() => { if (!data?.created_by) return false; return data?.created_by === user?.id; }, [data?.created_by, user?.id]); const className = APPROVAL_CONTROLLER_ROUTE[type]; const { product_id } = useApp(); const isFinopsPortal = useMemo( () => product_id === PRODUCT_IDENTIFIER.FINOPS, [product_id] ); const getStageLevels = useCallback(() => { return SortArrayObjectBy( GetUniqueObjectsFromArray( workflows?.map((workflow) => workflow.stage_level), 'id' ), 'level', 'desc' ).filter(Boolean); }, [workflows]); const availableApprovals = useMemo< { label: string; stage_level_id?: number; visible: boolean; }[] >(() => { const stageLevels = getStageLevels(); return stageLevels.map((stageLevel) => ({ label: stageLevel.name, stage_level_id: stageLevel.id, visible: true, })); }, [getStageLevels]); const skipLevel = async (id: number, next = () => {}) => { const { success, response } = await FetchData({ className, method: 'skipLevel', methodParams: { expenseId: document_id, id }, }); if (!success) { Toast.error({ description: response.message || 'Something went wrong!', }); return next(); } Toast.success({ description: 'Successfully skipped level!' }); callback(); next(); }; const dropSkipLevel = async (id: number, next = () => {}) => { const { success, response } = await FetchData({ className, method: 'dropSkipLevel', methodParams: { expenseId: document_id, id }, }); if (!success) { Toast.error({ description: response.message || 'Something went wrong!', }); return next(); } Toast.success({ description: 'Successfully skipped drop!' }); callback(); next(); }; const dropTransfer = async (id: number) => { const { success, response } = await FetchData({ className, method: 'dropTransfer', methodParams: { expenseId: document_id, id }, }); if (!success) { Toast.error({ description: response.message || 'Something went wrong!', }); return; } Toast.success({ description: 'Successfully Transfer Dropped.' }); callback(); }; const getWorkflows = useCallback( (level_id: number) => { return ( workflows?.filter( (workflow) => workflow.stage_level_id === level_id ) || [] ); }, [workflows] ); const getWorkflowStatus = useCallback((approval_type: number) => { // TODO: Add other statuses return false; }, []); const shouldShowReset = useMemo(() => { if (isAnyRejected || !currentActiveWorkflow) return false; if (IsEmptyArray(workflows)) return true; if (getWorkflowStatus(currentActiveWorkflow?.stage_level_id)) return false; const currentWorkflows = getWorkflows( currentActiveWorkflow?.stage_level_id ); if (IsEmptyArray(currentWorkflows)) return true; if (currentWorkflows[currentWorkflows.length - 1].activity_datetime) return true; return false; }, [ workflows, isAnyRejected, currentActiveWorkflow, getWorkflowStatus, getWorkflows, ]); const party_rejected = party_status_id === VerificationStatusTypeEnum.REJECTED; // if (IsEmptyArray(workflows)) { // return ( // <div className='flex items-center justify-center w-full h-full'> // <NoDataFound /> // </div> // ); // } return ( <div id='approval-workflows' className='w-full h-full gap-4 col-flex'> <div className='flex items-center justify-between'> <div className='font-medium text-base-primary'> Approval Workflows </div> </div> <div className='gap-4 pb-4 overflow-auto col-flex'> {availableApprovals.map((approval) => { if (!approval.visible) return null; const workflows = getWorkflows(approval.stage_level_id); const workflow_name = (() => { const workflow = workflows.find((workflow) => { return workflow.rule_id > 10; }); return workflow?.rule?.name; })(); return ( <div key={approval.stage_level_id} className='gap-4 p-4 border rounded-t bg-base-200 col-flex border-base-300' id={`approval-${approval.stage_level_id}`} aria-label={approval.label} > <div className='justify-between approval-workflow-header row-flex'> <div className='items-center gap-1 row-flex'> {getWorkflowStatus( approval.stage_level_id ) && ( <Icon source={VerifiedTickSvgIcon} iconColor='text-success' isSvg /> )} <h2 className='text-sm font-medium approval-workflow-title'> {approval.label} </h2> {!!workflow_name && ( <h3 className='text-sm font-medium approval-workflow-title'> ({workflow_name}) </h3> )} </div> </div> <div className='gap-3 col-flex'> {workflows.map((workflow, index) => { const isApproved = workflow.activity_id === ApprovalStatusTypeEnum.APPROVED; const isRejected = workflow.activity_id === ApprovalStatusTypeEnum.REJECTED; const isSkipped = !!workflow.is_skipped && !isApproved; const isInProgress = workflow.id === active_approval?.id; const canSkip = (() => { let skip = true; if (isSkipped || isApproved) return false; if (workflows.length - 1 === index) return false; for ( let i = workflows.length - 1; i >= 0; i-- ) { if (!skip) break; if (index < i) { skip = workflows[i].activity_id !== ApprovalStatusTypeEnum.APPROVED; } } return skip; })(); const groupTransfers = ( approvalList: ObjectDto[] ) => { const newList = []; approvalList.forEach((approval) => { if (!approval.parent_id) return newList.push(approval); const parentIndex = IndexOfObjectInArray( approvalList, 'id', approval.parent_id ); if (parentIndex > -1) { const parent = approvalList[parentIndex]; if ( !IsEmptyArray( approval.transferredApproval ) ) { if ( IsEmptyArray( parent.transferredApproval ) ) { parent.transferredApproval = [ ...approval.transferredApproval, ]; return; } parent.transferredApproval.push( ...approval.transferredApproval ); } } }); return newList; }; const approvals = (() => { const approvalList = []; SortArrayObjectBy( (workflow.workflow_users as any[]) || [], 'id', 'desc' ).forEach((approval, _, approvals) => { if (!approval?.user) return; if (approval.parent_id) { let approvalIndex = IndexOfObjectInArray( approvalList, 'id', approval.parent_id ); if (approvalIndex > -1) { approvalList[ approvalIndex ].transferredApproval = [ approval, ]; return; } approvalList.push({ ...GetObjectFromArray( approvals, 'id', approval.parent_id ), transferredApproval: [ approval, ], }); return; } if ( IndexOfObjectInArray( approvalList, 'id', approval.id ) === -1 ) { approvalList.push(approval); } }); return groupTransfers(approvalList); })(); const userGroupStatus = (() => { if ( isApproved || isRejected || isSkipped || workflow.approval_type_id !== ApprovalStepTypeEnum.USER_GROUP ) { return null; } const { property, users } = workflow?.attributes || {}; const approvedCount = approvals.filter( (approval) => !approval.parent_id && approval.activity_id === ApprovalStatusTypeEnum.APPROVED ).length; let toApproveCount = 1; if (property.all) { toApproveCount = users; } if (property.percentage) { toApproveCount = Math.ceil( (users * property.percentage) / 100 ); } return { total: toApproveCount, progress: approvedCount, }; })(); const canTransfer = (() => { let transfer = true; if (isApproved || isAnyRejected) return false; for ( let i = workflows.length - 1; i >= 0; i-- ) { if (!transfer) break; if (index < i) { transfer = workflows[i].activity_id !== ApprovalStatusTypeEnum.APPROVED; } } return transfer; })(); const getIcon = () => { if (isRejected) return 'close'; if (isApproved) return 'check'; if (isSkipped) return 'skip_next'; return ApprovalNonCheckedSvgIcon; }; const getIconAppearance = () => { if (isSkipped) return 'bg-info text-info-content'; if (isRejected) return 'bg-error text-error-content'; if (isApproved) return 'bg-success text-success-content'; return 'text-base-secondary'; }; return ( <Collapse key={workflow.id} title={ <div className='items-center justify-between row-flex'> <div className='items-center gap-6 row-flex'> <div className='items-center gap-2 row-flex'> <div className={cn( 'flex rounded-full bg-base-300 text-base-100 p-[2px] relative', getIconAppearance() )} > <Icon source={getIcon()} size={14} className='z-[1]' isSvg /> {isInProgress && !isSkipped && !party_rejected && ( <span className='absolute top-0 left-0 w-full h-full rounded-full opacity-50 animate-ping bg-primary'></span> )} </div> <span className='level-number'> Level{' '} {index + 1} </span> </div> <span className='level-name'> {workflow.name} </span> {userGroupStatus && ( <span className='level-progress-remaining'> {`${userGroupStatus.progress} / ${userGroupStatus.total} Remaining`} </span> )} </div> <div className='gap-2 row-flex'> {!isAnyRejected && !isApproved && canSkip && AccessManager.hasRoleIdentifier( 'ua_approval_overrider' ) && workflows.length > 1 && ( <Button className='opacity-0 group-hover/title:opacity-100' appearance='success' size='xs' progress onClick={( next, e ) => { e.preventDefault(); e.stopPropagation(); skipLevel( workflow.id, next ); }} > Skip </Button> )} {!isAnyRejected && isSkipped && !isApproved && !workflow ?.attributes ?.no_edit && AccessManager.hasRoleIdentifier( 'ua_approval_overrider' ) && ( <Button className='opacity-0 group-hover/title:opacity-100' appearance='error' size='xs' progress onClick={( next, e ) => { e.preventDefault(); e.stopPropagation(); dropSkipLevel( workflow.id, next ); }} > Drop Skip </Button> )} {/* {isApproved && ( <Badge size='md' appearance='success' label='Approved' /> )} {isRejected && ( <Badge size='md' appearance='error' label='Rejected' /> )} */} {/* {isSkipped && ( <Badge size='md' appearance='info' label='Skipped' /> )} */} {/* {isInProgress && ( <Badge size='md' appearance='warning' label='In Progress' /> )} */} {workflow.activity_datetime && FormatDisplayDateStyled( { value: workflow.activity_datetime, } )} </div> </div> } headerClassName='bg-base-100 p-3 px-4 gap-2 group/title workflow-level-header rounded' titleClassName='text-sm font-normal text-base-primary' defaultExpand={ isInProgress || (currentActiveWorkflow?.stage_level_id === approval.stage_level_id && index === 0) } > <ApprovalLevelUsers {...{ workflow, approvals, document_id, type, canTransfer, callback, dropTransfer, party_rejected, isFinopsPortal, isInProgress, isCreatorIsCurrentUser, }} /> </Collapse> ); })} </div> </div> ); })} </div> {shouldShowReset && !IsEmptyArray(workflows) && ( <div className='col-flex'> <div className='text-sm'> <a className='link link-hover' onClick={() => openOverrideApproval(data, type, callback) } > Click here </a>{' '} to override the approval workflow </div> </div> )} {shouldShowReset && IsEmptyArray(workflows) && ( <div className='col-flex'> {AccessManager.canEditExpense({ product_id, canApproveDocument: canApproveInvoice, data, }) ? ( <div className='text-sm'> <a className='link link-hover' onClick={openEdit}> Click here </a>{' '} to add poc to generate approval workflow. </div> ) : ( <div className='text-sm'> Contact your expense manager to add correct poc to create approval workflow </div> )} </div> )} {!shouldShowReset && IsEmptyArray(workflows) && ( <NoDataFound title='No workflows found' /> )} </div> ); }; const ApprovalLevelUsers = ({ approvals, workflow, document_id, type, canTransfer, party_rejected, dropTransfer, callback, isFinopsPortal, isInProgress, isCreatorIsCurrentUser, }: { document_id: number; type: ApprovalListingType; workflow: ObjectDto; approvals: ObjectDto[]; canTransfer?: boolean; party_rejected?: boolean; dropTransfer?: (id: number) => void; callback?: Function; isFinopsPortal?: boolean; isInProgress?: boolean; isCreatorIsCurrentUser?: boolean; }) => { return ( <div className='border col-flex approvals'> {approvals.map((approval) => { const transferredApprovalLength = approval?.transferredApproval?.length; return ( <div key={approval.id} className='p-4 border-b rounded-b approval col-flex last:border-b-0 bg-base-100' > <div className='gap-4 col-flex'> <ApprovalUserCard {...{ document_id, type, workflow, approval, canTransfer, party_rejected, dropTransfer, isTransferred: !IsEmptyArray( approval.transferredApproval ), isFinopsPortal, callback, isInProgress, isCreatorIsCurrentUser, transferId: !IsEmptyArray( approval.transferredApproval ) ? approval?.transferredApproval[ transferredApprovalLength - 1 ]?.id : approval.id, }} /> {!IsEmptyArray(approval.transferredApproval) ? ( <> {approval.transferredApproval.map( ( tApproval: ObjectDto, index: number, tArray: ObjectDto[] ) => { return ( <ApprovalUserCard key={index} {...{ document_id, type, workflow, approval: tApproval, canTransfer, party_rejected, dropTransfer, isTransferred: tArray.length - 1 > index, callback, isFinopsPortal, isInProgress, isCreatorIsCurrentUser, }} /> ); } )} </> ) : null} </div> {approval.attributes?.reason || approval.comments ? ( <div className='w-full my-2 border-b border-dashed' /> ) : null} {approval.attributes?.reason ? ( <div className='text-sm text-base-secondary'> Reason :{' '} <span className='text-base-primary'> {approval.attributes?.reason} </span> </div> ) : null} {approval.comments ? ( <div className='text-sm text-base-secondary'> Comments :{' '} <span className='text-base-primary'> {approval.comments} </span> </div> ) : null} </div> ); })} </div> ); }; const ApprovalUserCard = ({ document_id, type, workflow, approval, canTransfer, party_rejected, isTransferred, dropTransfer, callback, isFinopsPortal, isInProgress, isCreatorIsCurrentUser, transferId, }: { document_id: number; type: ApprovalListingType; workflow: ObjectDto; approval: ObjectDto; canTransfer?: boolean; party_rejected?: boolean; isTransferred?: boolean; dropTransfer?: (id: number) => void; callback?: Function; isFinopsPortal?: boolean; isInProgress?: boolean; isCreatorIsCurrentUser?: boolean; transferId?: number; }) => { const { user, employee } = approval; const { image_url } = user || {}; const { isSuccess: handleSuccessEmployeNavigation } = useGetEmployeeId(); const actions = [ { name: 'Transfer', visible: !!canTransfer && !approval.parent_id && IsEmptyArray(approval.transferredApproval) && approval?.activity_id !== ApprovalStatusTypeEnum.APPROVED && AccessManager.hasRoleIdentifier('ua_approval_overrider'), action: () => { openTransferApproval({ document_id, type, level_id: approval.id, current_user_id: approval.user_id, callback: () => { SlidingPane.close(); callback(); }, }); }, }, { name: 'Delete Transfer', visible: !!canTransfer && !IsEmptyArray(approval.transferredApproval) && approval.transferredApproval.length === 1 && AccessManager.hasRoleIdentifier('ua_approval_overrider'), action: () => dropTransfer(approval.transferredApproval[0].id), }, { name: 'Send Notification', action: () => { ConfirmUtil({ message: 'Are you sure you want to send notification?', title: 'Send Notification', cancelAppearance: 'error', appearance: 'success', onConfirmPress: () => { SendNotification({ typeId: document_id, type, approvalId: transferId ?? approval.id, }); }, }); }, visible: isInProgress && isCreatorIsCurrentUser, }, ]; return ( <div key={approval?.id} className={cn( 'approval-user text-sm approve-card col-flex flex-1', handleActivityTimeline(approval?.activity_id) )} > <div className='grid grid-cols-3 gap-4'> <div className={cn(' gap-3 row-flex', { 'items-center': image_url, })} > {image_url ? ( <Avatar source={image_url} alt={employee?.name || user?.name} size='32' shape='rounded' color='vendor' /> ) : ( <EmployeeIconImage className='w-8 h-8' size={18} /> )} <div className='col-flex'> <button onClick={(e) => { if (!isFinopsPortal) return; handleNavigationEmployeeDetail( handleSuccessEmployeNavigation, user.id ); }} className={cn( 'flex items-center gap-2 font-medium approval-user-name ', isFinopsPortal && 'table-link' )} > {employee?.name || user?.name} </button> <span className='text-xs approval-user-email'> {user?.email} </span> </div> </div> <div className='text-center approval-status'> {!isTransferred && approval.activity_datetime && ( <> {approval?.activity_id === ApprovalStatusTypeEnum.APPROVED && ( <div className='text-success'>Approved</div> )} {approval?.activity_id === ApprovalStatusTypeEnum.REJECTED && ( <div className='text-error'>Rejected</div> )} </> )} {!isTransferred && !approval.activity_datetime && ( <> {!party_rejected && !workflow.is_skipped && !workflow.activity_datetime && ( <div className={'text-warning'}> Pending Approval </div> )} {!!workflow.is_skipped && ( <div className='text-info'>Skipped</div> )} </> )} {isTransferred && ( <div className={'text-info'}>Transferred</div> )} </div> <div className='justify-between gap-2 row-flex'> <div className='flex-1 gap-1 text-right col-flex'> {approval.activity_datetime ? ( <span className='text-xs text-base-primary'> {FormatDisplayDate( approval.activity_datetime, true )} </span> ) : null} {!IsUndefinedOrNull(approval.attributes?.limit) ? ( <div className={'text-xs text-base-secondary'}> Threshold :{' '} <span className='text-base-primary'> {FormatCurrency({ amount: approval.attributes?.limit || 0, })}{' '} & Above </span> </div> ) : null} </div> {!approval.parent_id && ![ ApprovalStatusTypeEnum.APPROVED, ApprovalStatusTypeEnum.REJECTED, ].includes(approval?.activity_id) && actions.some((action) => action.visible !== false) && ( <div> <DropdownMenu actions={actions} align='end'> <div className='p-2 border rounded cursor-pointer select-none hover:bg-base-200 approval-actions'> <MoreVertical size={16} /> </div> </DropdownMenu> </div> )} </div> </div> </div> ); }; export default InvoiceApprovalWorkflow; const SendNotification = async ({ typeId, type, approvalId, }: { typeId: number; type: string; approvalId: number; }) => { const controllers = { expense: ExpenseApprovalController, advance: EmployeeAdvanceApprovalController, purchase_request: PurchaseRequestApprovalController, purchase_order: PurchaseOrderApprovalController, project: ProjectApprovalController, }; const className = controllers[type]; const { success, response } = await FetchData({ className, methodParams: { expenseId: typeId, id: approvalId, }, method: 'notify', }); if (!success) return toastBackendError(response, 'Something went wrong!'); Toast.success({ description: 'Notification sent successfully', }); };
Leave a Comment