feat(FE-331): implement permission guard in project flock

This commit is contained in:
ValdiANS
2025-12-26 21:08:00 +07:00
parent 4be719b9d8
commit 9e0d3e2bbf
5 changed files with 242 additions and 173 deletions
+30 -2
View File
@@ -5,6 +5,8 @@ import Tooltip from '@/components/Tooltip';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useAuth } from '@/services/hooks/useAuth';
type FloatingActionsButtonProps = { type FloatingActionsButtonProps = {
actions: { actions: {
action: 'DETAIL' | 'EDIT' | 'DELETE'; action: 'DETAIL' | 'EDIT' | 'DELETE';
@@ -13,6 +15,7 @@ type FloatingActionsButtonProps = {
onClick?: () => void; onClick?: () => void;
hidden?: boolean; hidden?: boolean;
disabled?: boolean; disabled?: boolean;
permissions?: string | string[];
}[]; }[];
approvals: { approvals: {
action: 'APPROVED' | 'REJECTED'; action: 'APPROVED' | 'REJECTED';
@@ -20,6 +23,7 @@ type FloatingActionsButtonProps = {
label?: string; label?: string;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
permissions?: string | string[];
}[]; }[];
selectedRowIds: number[]; selectedRowIds: number[];
onClose: () => void; onClose: () => void;
@@ -31,6 +35,7 @@ const FloatingActionsButton = ({
selectedRowIds, selectedRowIds,
onClose, onClose,
}: FloatingActionsButtonProps) => { }: FloatingActionsButtonProps) => {
const { permissionCheck } = useAuth();
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB // Jika tidak ada baris yang dipilih, jangan tampilkan FAB
const positionStyles = const positionStyles =
selectedRowIds.length > 0 selectedRowIds.length > 0
@@ -71,7 +76,18 @@ const FloatingActionsButton = ({
<div className='flex gap-4 items-center'> <div className='flex gap-4 items-center'>
{/* Render Aksi dari props.actions */} {/* Render Aksi dari props.actions */}
{actions {actions
.filter((action) => !action.hidden) .filter((action) => {
if (action.hidden) return false;
if (action.permissions) {
if (typeof action.permissions === 'string') {
return permissionCheck(action.permissions);
}
return action.permissions.some((permission) =>
permissionCheck(permission)
);
}
return true;
})
.map((action, index) => { .map((action, index) => {
return ( return (
<Button <Button
@@ -111,7 +127,19 @@ const FloatingActionsButton = ({
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */} {/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
<div className={`grid grid-cols-${approvals.length} gap-3`}> <div className={`grid grid-cols-${approvals.length} gap-3`}>
{approvals.map((approval, index) => ( {approvals
.filter((approval) => {
if (approval.permissions) {
if (typeof approval.permissions === 'string') {
return permissionCheck(approval.permissions);
}
return approval.permissions.some((permission) =>
permissionCheck(permission)
);
}
return true;
})
.map((approval, index) => (
<Button <Button
key={index} key={index}
onClick={approval.onClick} onClick={approval.onClick}
@@ -25,6 +25,8 @@ import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', type = 'dropdown',
props, props,
@@ -46,6 +48,7 @@ const RowOptionsMenu = ({
)} )}
> >
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<RequirePermission permissions='lti.production.project_flocks.detail'>
<Button <Button
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`} href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
@@ -55,7 +58,9 @@ const RowOptionsMenu = ({
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='mdi:eye-outline' width={16} height={16} />
Detail Detail
</Button> </Button>
</RequirePermission>
{props.row.original.approval.step_name === 'Aktif' && ( {props.row.original.approval.step_name === 'Aktif' && (
<RequirePermission permissions='lti.production.chickins.create'>
<Button <Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`} href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
@@ -65,8 +70,10 @@ const RowOptionsMenu = ({
<Icon icon='mdi:home-import-outline' width={16} height={16} /> <Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin Chickin
</Button> </Button>
</RequirePermission>
)} )}
{props.row.original.approval.step_name === 'Pengajuan' && ( {props.row.original.approval.step_name === 'Pengajuan' && (
<RequirePermission permissions='lti.production.project_flocks.update'>
<Button <Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`} href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
@@ -76,7 +83,9 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit Edit
</Button> </Button>
</RequirePermission>
)} )}
<RequirePermission permissions='lti.production.project_flocks.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
@@ -90,6 +99,7 @@ const RowOptionsMenu = ({
/> />
Delete Delete
</Button> </Button>
</RequirePermission>
</div> </div>
</div> </div>
); );
@@ -287,6 +297,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col justify-between items-end gap-2'> <div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'> <div className='flex flex-col sm:flex-row gap-3 w-full'>
<RequirePermission permissions='lti.production.project_flocks.create'>
<Button <Button
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
@@ -295,6 +306,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='ic:round-plus' width={24} height={24} />
Tambah Tambah
</Button> </Button>
</RequirePermission>
{/* <Button {/* <Button
variant='outline' variant='outline'
color='success' color='success'
@@ -630,6 +642,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
setRowSelection({}); setRowSelection({});
}, },
permissions: 'lti.production.project_flocks.detail',
}, },
{ {
action: 'DELETE', action: 'DELETE',
@@ -639,6 +652,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
onClick: () => { onClick: () => {
deleteModal.openModal(); deleteModal.openModal();
}, },
permissions: 'lti.production.project_flocks.delete',
}, },
]} ]}
approvals={[ approvals={[
@@ -651,6 +665,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
confirmModal.openModal(); confirmModal.openModal();
}, },
disabled: !canApprove, disabled: !canApprove,
permissions: 'lti.production.project_flocks.approve',
}, },
{ {
icon: 'mdi:times', icon: 'mdi:times',
@@ -660,6 +675,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setApprovalAction('REJECTED'); setApprovalAction('REJECTED');
confirmModal.openModal(); confirmModal.openModal();
}, },
permissions: 'lti.production.project_flocks.approve',
}, },
]} ]}
selectedRowIds={selectedRowIds} selectedRowIds={selectedRowIds}
@@ -22,6 +22,7 @@ import { useEffect, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { FormHeader } from '@/components/helper/form/FormHeader'; import { FormHeader } from '@/components/helper/form/FormHeader';
import Link from 'next/link'; import Link from 'next/link';
import RequirePermission from '@/components/helper/RequirePermission';
const ProjectFlockChickinDetail = ({ const ProjectFlockChickinDetail = ({
projectFlockId, projectFlockId,
@@ -484,6 +485,7 @@ const ProjectFlockChickinDetail = ({
{kandang.kandang.name} {kandang.kandang.name}
</span> </span>
</div> </div>
<RequirePermission permissions='lti.production.chickins.create'>
<Button <Button
variant='outline' variant='outline'
className='py-1 text-sm' className='py-1 text-sm'
@@ -499,6 +501,7 @@ const ProjectFlockChickinDetail = ({
height={11} height={11}
/> />
</Button> </Button>
</RequirePermission>
</div> </div>
))} ))}
</div> </div>
@@ -29,6 +29,7 @@ import {
} from '@/config/approval-line'; } from '@/config/approval-line';
import useSWR from 'swr'; import useSWR from 'swr';
import { ProjectFlockKandangApi } from '@/services/api/production'; import { ProjectFlockKandangApi } from '@/services/api/production';
import RequirePermission from '@/components/helper/RequirePermission';
const ProjectFlockDetail = ({ const ProjectFlockDetail = ({
projectFlock, projectFlock,
@@ -110,6 +111,7 @@ const ProjectFlockDetail = ({
leftIconHref='/production/project-flock' leftIconHref='/production/project-flock'
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`} subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
> >
<RequirePermission permissions='lti.production.project_flocks.update'>
<Link <Link
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`} href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
className='p-0' className='p-0'
@@ -120,6 +122,8 @@ const ProjectFlockDetail = ({
</Button> </Button>
</Tooltip> </Tooltip>
</Link> </Link>
</RequirePermission>
<RequirePermission permissions='lti.production.project_flocks.delete'>
<Button <Button
variant='link' variant='link'
className='p-0 text-error' className='p-0 text-error'
@@ -131,6 +135,7 @@ const ProjectFlockDetail = ({
<Icon icon='mdi:trash-can-outline' width={20} height={20} /> <Icon icon='mdi:trash-can-outline' width={20} height={20} />
</Tooltip> </Tooltip>
</Button> </Button>
</RequirePermission>
</DrawerHeader> </DrawerHeader>
{/* Informasi Umum */} {/* Informasi Umum */}
@@ -418,6 +423,7 @@ const ProjectFlockDetail = ({
</RadioGroup> </RadioGroup>
</Card> </Card>
<div className='grid grid-cols-4 gap-3'> <div className='grid grid-cols-4 gap-3'>
<RequirePermission permissions='lti.production.chickins.create'>
<Link <Link
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`} href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
className='m-0 p-0' className='m-0 p-0'
@@ -434,6 +440,8 @@ const ProjectFlockDetail = ({
Chickin <Icon icon='mdi:checkbox-marked-outline' /> Chickin <Icon icon='mdi:checkbox-marked-outline' />
</Button> </Button>
</Link> </Link>
</RequirePermission>
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
<Link <Link
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`} href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
className='m-0 p-0' className='m-0 p-0'
@@ -450,6 +458,7 @@ const ProjectFlockDetail = ({
Close <Icon icon='mdi:checkbox-marked-circle-outline' /> Close <Icon icon='mdi:checkbox-marked-circle-outline' />
</Button> </Button>
</Link> </Link>
</RequirePermission>
</div> </div>
</div> </div>
</div> </div>
@@ -47,6 +47,7 @@ import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
import { Nonstock } from '@/types/api/master-data/nonstock'; import { Nonstock } from '@/types/api/master-data/nonstock';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import RequirePermission from '@/components/helper/RequirePermission';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
interface ProjectFlockFormProps { interface ProjectFlockFormProps {
@@ -734,6 +735,7 @@ const ProjectFlockForm = ({
)} )}
{formType == 'detail' && ( {formType == 'detail' && (
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'> <div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button <Button
variant='outline' variant='outline'
color='success' color='success'
@@ -749,6 +751,8 @@ const ProjectFlockForm = ({
<Icon icon='material-symbols:check' width={24} height={24} /> <Icon icon='material-symbols:check' width={24} height={24} />
Approve Approve
</Button> </Button>
</RequirePermission>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button <Button
variant='outline' variant='outline'
color='error' color='error'
@@ -764,6 +768,7 @@ const ProjectFlockForm = ({
<Icon icon='mdi:times' width={24} height={24} /> <Icon icon='mdi:times' width={24} height={24} />
Reject Reject
</Button> </Button>
</RequirePermission>
</div> </div>
)} )}
<form <form
@@ -1127,6 +1132,13 @@ const ProjectFlockForm = ({
</div> </div>
</div> */} </div> */}
{formType !== 'detail' && ( {formType !== 'detail' && (
<RequirePermission
permissions={
formType == 'add'
? 'lti.production.project_flocks.create'
: 'lti.production.project_flocks.update'
}
>
<Button <Button
type='submit' type='submit'
color='primary' color='primary'
@@ -1137,6 +1149,7 @@ const ProjectFlockForm = ({
<Icon icon='mdi:plus' width={24} height={24} /> <Icon icon='mdi:plus' width={24} height={24} />
{formType == 'add' ? 'Add Flock' : 'Update Flock'} {formType == 'add' ? 'Add Flock' : 'Update Flock'}
</Button> </Button>
</RequirePermission>
)} )}
</div> </div>
</form> </form>