Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-76/TASK-114-129-136-slicing-ui-and-validation-create-edit-daily-recording-growing-form

This commit is contained in:
rstubryan
2025-10-20 10:09:18 +07:00
8 changed files with 254 additions and 2 deletions
+1
View File
@@ -1,5 +1,6 @@
@import 'tailwindcss';
@plugin "daisyui";
@import '../styles/daisyui.css';
:root {
--color-primary: #1f74bf;
+60
View File
@@ -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 (
<div className={cn(tooltipBaseClassName, className?.wrapper)}>
<div
className={cn(
'tooltip-content',
'max-w-60 sm:max-w-xs',
className?.content
)}
>
{content}
</div>
{children}
</div>
);
};
export default Tooltip;
+64
View File
@@ -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 (
<Steps direction='vertical' className='w-full md:steps-horizontal'>
{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 (
<StepItem
key={idx}
color={stepItemColor}
icon={
approval.status !== 'waiting' && (
<Tooltip
color={stepItemColor}
position='right'
className={{
wrapper: 'md:tooltip-bottom',
}}
content={
<div className='flex flex-col text-base'>
<span>{formatDate(approval.date, 'YYYY-MM-DD')}</span>
<span>Oleh: {approval.action_by}</span>
<span>Catatan: {approval.notes}</span>
</div>
}
>
<Icon icon={stepItemIcon} width={24} height={24} />
</Tooltip>
)
}
>
{approval.role}
</StepItem>
);
})}
</Steps>
);
};
export default ApprovalSteps;
+34
View File
@@ -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 (
<li className={cn(stepItemBaseClassName, className)}>
<span className='step-icon'>{icon}</span>
<div>{children}</div>
</li>
);
};
export default StepItem;
+23
View File
@@ -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 (
<ul className={cn(stepsBaseClassName, 'overflow-visible!', className)}>
{children}
</ul>
);
};
export default Steps;
+23 -2
View File
@@ -6,22 +6,43 @@ type AuthStore = {
isLoadingUser?: boolean;
setUser: (newUserData?: UserWithRoles) => void;
setIsLoadingUser: (isLoading?: boolean) => void;
permissionCheck: (permissionName: string) => boolean;
};
const useAuthStore = create<AuthStore>()((set) => ({
const useAuthStore = create<AuthStore>()((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,
};
};
+11
View File
@@ -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);
}
}
+38
View File
@@ -24,6 +24,36 @@ export type LogoutResponse = BaseApiResponse;
export type GetMeResponse = BaseApiResponse<UserWithRoles>;
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<Client, 'created_at' | 'updated_at'>;
created_at: string;
updated_at: string;
};
export type Role = {
id: number;
key: string;
name: string;
client: Omit<Client, 'created_at' | 'updated_at'>;
created_at: string;
updated_at: string;
};
export type RoleWithPermissions = Omit<Role, 'created_at' | 'updated_at'> & {
permissions: Omit<Permission, 'created_at' | 'updated_at'>[];
};
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';
}[];