diff --git a/src/app/globals.css b/src/app/globals.css index 386e7620..79af241b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,6 @@ @import 'tailwindcss'; @plugin "daisyui"; +@import '../styles/daisyui.css'; :root { --color-primary: #1f74bf; diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx new file mode 100644 index 00000000..02f86dca --- /dev/null +++ b/src/components/Tooltip.tsx @@ -0,0 +1,60 @@ +import { ReactNode } from 'react'; + +import { cn } from '@/lib/helper'; +import { Color } from '@/types/theme'; + +interface TooltipProps { + children?: ReactNode; + content?: ReactNode; + className?: { + wrapper?: string; + content?: string; + }; + open?: boolean; + color?: Color; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +const Tooltip = ({ + children, + content, + className, + open, + color, + position, +}: TooltipProps) => { + const tooltipBaseClassName = cn('tooltip', { + 'tooltip-open': typeof open === 'boolean' && open, + + 'tooltip-top': position === 'top', + 'tooltip-bottom': position === 'bottom', + 'tooltip-left': position === 'left', + 'tooltip-right': position === 'right', + + 'tooltip-primary': color === 'primary', + 'tooltip-secondary': color === 'secondary', + 'tooltip-accent': color === 'accent', + 'tooltip-neutral': color === 'neutral', + 'tooltip-info': color === 'info', + 'tooltip-success': color === 'success', + 'tooltip-warning': color === 'warning', + 'tooltip-error': color === 'error', + }); + return ( +
+
+ {content} +
+ + {children} +
+ ); +}; + +export default Tooltip; diff --git a/src/components/pages/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx new file mode 100644 index 00000000..4022e254 --- /dev/null +++ b/src/components/pages/ApprovalSteps.tsx @@ -0,0 +1,64 @@ +import { Icon } from '@iconify/react'; +import Steps from '@/components/steps/Steps'; +import StepItem from '@/components/steps/StepItem'; +import Tooltip from '@/components/Tooltip'; + +import { formatDate } from '@/lib/helper'; +import { ApprovalsLine } from '@/types/api/api-general'; + +interface ApprovalStepsProps { + approvals: ApprovalsLine; +} + +const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => { + return ( + + {approvals.map((approval, idx) => { + const stepItemColor = + approval.status === 'approved' + ? 'success' + : approval.status === 'rejected' + ? 'error' + : undefined; + + const stepItemIcon = + approval.status === 'approved' + ? 'material-symbols:check-rounded' + : approval.status === 'rejected' + ? 'material-symbols:close-rounded' + : 'bxs:hourglass'; + + return ( + + {formatDate(approval.date, 'YYYY-MM-DD')} + Oleh: {approval.action_by} + Catatan: {approval.notes} + + } + > + + + ) + } + > + {approval.role} + + ); + })} + + ); +}; + +export default ApprovalSteps; diff --git a/src/components/steps/StepItem.tsx b/src/components/steps/StepItem.tsx new file mode 100644 index 00000000..85ec4f3e --- /dev/null +++ b/src/components/steps/StepItem.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from 'react'; + +import { cn } from '@/lib/helper'; +import { Color } from '@/types/theme'; + +interface StepItemProps { + children?: ReactNode; + icon?: ReactNode; + className?: string; + color?: Color; +} + +const StepItem = ({ children, icon, className, color }: StepItemProps) => { + const stepItemBaseClassName = cn('step', { + 'step-primary': color === 'primary', + 'step-secondary': color === 'secondary', + 'step-accent': color === 'accent', + 'step-neutral': color === 'neutral', + 'step-info': color === 'info', + 'step-success': color === 'success', + 'step-warning': color === 'warning', + 'step-error': color === 'error', + }); + + return ( +
  • + {icon} + +
    {children}
    +
  • + ); +}; + +export default StepItem; diff --git a/src/components/steps/Steps.tsx b/src/components/steps/Steps.tsx new file mode 100644 index 00000000..29d307e1 --- /dev/null +++ b/src/components/steps/Steps.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react'; +import { cn } from '@/lib/helper'; + +interface StepsProps { + children?: ReactNode; + className?: string; + direction?: 'horizontal' | 'vertical'; +} + +const Steps = ({ children, className, direction }: StepsProps) => { + const stepsBaseClassName = cn('steps gap-2', { + 'steps-horizontal': direction === 'horizontal', + 'steps-vertical': direction === 'vertical', + }); + + return ( + + ); +}; + +export default Steps; diff --git a/src/services/hooks/useAuth.tsx b/src/services/hooks/useAuth.tsx index 86bf43ed..79fa8981 100644 --- a/src/services/hooks/useAuth.tsx +++ b/src/services/hooks/useAuth.tsx @@ -6,22 +6,43 @@ type AuthStore = { isLoadingUser?: boolean; setUser: (newUserData?: UserWithRoles) => void; setIsLoadingUser: (isLoading?: boolean) => void; + permissionCheck: (permissionName: string) => boolean; }; -const useAuthStore = create()((set) => ({ +const useAuthStore = create()((set, get) => ({ user: undefined, isLoadingUser: false, setUser: (newUserData) => set({ user: newUserData }), setIsLoadingUser: (isLoading) => set({ isLoadingUser: Boolean(isLoading) }), + + permissionCheck: (name) => { + const { user, isLoadingUser } = get(); + + if (!isLoadingUser && user) { + const isAllowed = user.roles.some((role) => { + const isPermissionNameAllowed = role.permissions.some( + (permission) => permission.name === name + ); + + return isPermissionNameAllowed; + }); + + return isAllowed; + } + + return false; + }, })); export const useAuth = () => { - const { user, setUser, isLoadingUser, setIsLoadingUser } = useAuthStore(); + const { user, setUser, isLoadingUser, setIsLoadingUser, permissionCheck } = + useAuthStore(); return { user, setUser, isLoadingUser, setIsLoadingUser, + permissionCheck, }; }; diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css new file mode 100644 index 00000000..9a148fb4 --- /dev/null +++ b/src/styles/daisyui.css @@ -0,0 +1,11 @@ +@layer utilities { + .step.step-success::before { + --step-bg: var(--color-success); + --step-fg: var(--color-success-content); + } + + .step.step-error::before { + --step-bg: var(--color-error); + --step-fg: var(--color-error-content); + } +} diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index 6a3fc6be..c118b5a4 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -24,6 +24,36 @@ export type LogoutResponse = BaseApiResponse; export type GetMeResponse = BaseApiResponse; +export type Client = { + id: number; + name: stirng; + alias: string; + created_at: string; + updated_at: string; +}; + +export type Permission = { + id: number; + name: string; + action: string; + client: Omit; + created_at: string; + updated_at: string; +}; + +export type Role = { + id: number; + key: string; + name: string; + client: Omit; + created_at: string; + updated_at: string; +}; + +export type RoleWithPermissions = Omit & { + permissions: Omit[]; +}; + export type User = { id: number; email: string; @@ -66,3 +96,11 @@ export type flags = | 'STARTER' | 'FINISHER' | 'OVK'; + +export type ApprovalsLine = { + action_by?: string; + date?: string; + notes?: string; + role?: string; + status: 'approved' | 'rejected' | 'waiting'; +}[];