Compare commits

..

2 Commits

9 changed files with 20 additions and 68 deletions

View File

@ -50,7 +50,6 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
<li key={item.name} className="mb-1 relative group"> <li key={item.name} className="mb-1 relative group">
{hasChildren ? ( {hasChildren ? (
<button <button
// Toggle pin on click
onClick={() => setPinnedFlyout(prev => prev === item.name ? null : item.name)} onClick={() => setPinnedFlyout(prev => prev === item.name ? null : item.name)}
className={`w-full flex items-center justify-center px-4 py-3 transition-colors duration-200 ${isChildActive || isPinned ? 'text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent' className={`w-full flex items-center justify-center px-4 py-3 transition-colors duration-200 ${isChildActive || isPinned ? 'text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
}`} }`}
@ -67,8 +66,6 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
</Link> </Link>
)} )}
{/* Flyout Submenu or Tooltip on Hover/Pin */}
{/* If pinned, it forces 'block'. If not pinned, 'hidden' but 'group-hover:block' shows it on hover */}
<div <div
className={`${isPinned ? 'block' : 'hidden'} group-hover:block absolute left-full ml-4 top-0 z-50 shadow-lg`} className={`${isPinned ? 'block' : 'hidden'} group-hover:block absolute left-full ml-4 top-0 z-50 shadow-lg`}
> >
@ -137,8 +134,12 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
<div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}> <div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}>
<div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}> <div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
{!isCollapsed && ( {!isCollapsed && (
<Link to="/dashboard" className="flex items-center flex-shrink-0"> <Link to="/dashboard" className="flex items-center flex-1 overflow-hidden">
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" /> <img
src="/logo.gif"
alt="Company Logo"
className="h-14 w-5xl max-w-75 cursor-pointer "
/>
</Link> </Link>
)} )}
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors flex-shrink-0"> <button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors flex-shrink-0">
@ -146,11 +147,6 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
</button> </button>
</div> </div>
{/*
Conditional Overflow Logic:
- Collapsed: 'overflow-x-visible' allows flyout menus to appear outside the sidebar bounds.
- Expanded: 'overflow-y-auto overflow-x-hidden' enables vertical scrolling to see all dropdowns.
*/}
<nav className={`flex-1 mt-4 ${isCollapsed ? 'overflow-x-visible' : 'overflow-y-auto overflow-x-hidden'}`}> <nav className={`flex-1 mt-4 ${isCollapsed ? 'overflow-x-visible' : 'overflow-y-auto overflow-x-hidden'}`}>
<ul> <ul>
{navItems.map(item => renderNavItem(item))} {navItems.map(item => renderNavItem(item))}

View File

@ -33,7 +33,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
]; ];
} }
// Shared children for HRMANAGER and DIRECTOR
const attendanceSummaryItems: NavItem = { const attendanceSummaryItems: NavItem = {
name: 'Attendance Service', name: 'Attendance Service',
icon: 'BarChart3', icon: 'BarChart3',
@ -70,7 +69,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
] ]
}; };
// 3. HRMANAGER
if (role === 'hrmanager') { if (role === 'hrmanager') {
return [ return [
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }, { name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
@ -81,7 +79,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
]; ];
} }
// 4. DIRECTOR
if (role === 'director') { if (role === 'director') {
return [ return [
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }, { name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },

View File

@ -33,7 +33,7 @@ export default function MonthlyReportFilters({ filters, onFilterChange }: Monthl
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
let newFilters = { ...filters, [name]: value }; const newFilters = { ...filters, [name]: value };
// Reset child dropdowns if parent changes // Reset child dropdowns if parent changes
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; } if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }

View File

@ -20,7 +20,6 @@ export default function DailyReport() {
employeeCode: '', employeeCode: '',
}); });
// Fetch data using the hook
const { data: reportData, isLoading, isFetching } = useDailyReport(filters); const { data: reportData, isLoading, isFetching } = useDailyReport(filters);
const handleDownload = () => { const handleDownload = () => {

View File

@ -5,8 +5,6 @@ import ErrorState from '../../../components/ui/ErrorState';
import RegularizationTable from '../components/summary/RegularizationTable'; import RegularizationTable from '../components/summary/RegularizationTable';
import { useHRManagerRegularizations, type RegularizationRecord } from '../api/useAttendanceData'; import { useHRManagerRegularizations, type RegularizationRecord } from '../api/useAttendanceData';
// In a real app, this would call the /api/ams/attendance/regularize/review endpoint
// For now, we just update the local state to show it working
const mockReviewAction = (records: RegularizationRecord[], id: string, newStatus: 'Approved' | 'Rejected') => { const mockReviewAction = (records: RegularizationRecord[], id: string, newStatus: 'Approved' | 'Rejected') => {
return records.map(r => r.id === id ? { ...r, status: newStatus } : r); return records.map(r => r.id === id ? { ...r, status: newStatus } : r);
}; };
@ -18,6 +16,7 @@ export default function HRManagerRegularization() {
// Once data is loaded, sync it to local state so we can update statuses without refetching immediately // Once data is loaded, sync it to local state so we can update statuses without refetching immediately
useMemo(() => { useMemo(() => {
if (initialRecords) { if (initialRecords) {
// eslint-disable-next-line react-hooks/set-state-in-render
setLocalRecords(initialRecords); setLocalRecords(initialRecords);
} }
}, [initialRecords]); }, [initialRecords]);

View File

@ -26,7 +26,6 @@ export default function MonthlyReport() {
employeeCode: '', employeeCode: '',
}); });
// Fetch data using the hook
const { data: reportData, isLoading, isFetching } = useAdminReport(filters); const { data: reportData, isLoading, isFetching } = useAdminReport(filters);
const handleDownload = () => { const handleDownload = () => {
@ -57,7 +56,6 @@ export default function MonthlyReport() {
<MonthlyReportFilters filters={filters} onFilterChange={setFilters} /> <MonthlyReportFilters filters={filters} onFilterChange={setFilters} />
{/* Data Table */}
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto"> <div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
<h3 className="text-lg font-semibold text-text-primary mb-4"> <h3 className="text-lg font-semibold text-text-primary mb-4">
Filtered Results ({reportData?.length || 0}) Filtered Results ({reportData?.length || 0})

View File

@ -1,40 +1,5 @@
import MyAttendance from '../pages/MyAttendance'; import MyAttendance from '../pages/MyAttendance';
export default function AttendanceRouter() { export default function AttendanceRouter() {
// Since MyAttendance pulls the ID dynamically from Redux,
// it works perfectly for Employee, Manager, HR, and Director.
return <MyAttendance />; return <MyAttendance />;
} }
// ### How are we ensuring Managers/HR use ONLY their data?
// We are achieving data isolation through a 3-layer security design that matches your backend developer's architecture perfectly:
// #### Layer 1: Frontend State (Redux)
// When you log in as a Manager, Redux sets the `mockUserId` to `127` (or whatever the Manager's actual ID is).
// ```typescript
// // From roleSlice.ts
// manager: { userId: '127', branches: '1' }
// ```
// The `MyAttendance` component reads this ID:
// ```typescript
// const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId));
// ```
// Because it reads from Redux, the frontend *only* ever asks the backend for the logged-in user's data. A Manager cannot trigger an API call for Employee `126` because the frontend simply doesn't have `126` in its state.
// #### Layer 2: The Axios Interceptor (Mock SSO)
// When the API call leaves the frontend, your `client.ts` Axios interceptor attaches the Mock SSO headers:
// ```typescript
// config.headers['X-Mock-User-Id'] = mockUserId; // e.g., "127"
// config.headers['X-Mock-User-Role'] = mockUserRole; // e.g., "MANAGER"
// ```
// #### Layer 3: The API Gateway (The True Enforcer)
// Even if a malicious Manager somehow modified their browser code to send `?employee_id=126` in the URL parameters, **the backend API Gateway will reject it.**
// According to your architecture diagram, the Gateway reads the `X-Mock-User-Id` and `X-Mock-User-Role` headers.
// * The Gateway sees: "A request came in for Employee 126's attendance, but the headers say this is User 127 acting as a MANAGER."
// * The Gateway throws a **403 Forbidden** error because the token ID (127) does not match the requested ID (126).
// *(When real SSO/Keycloak is implemented, this exact same logic applies, just using the JWT token payload instead of mock headers).*
// By standardizing to `MyAttendance` and relying on Redux for the ID, your code is clean, DRY (Don't Repeat Yourself), and completely secure!

View File

@ -1,4 +1,3 @@
// src/features/attendance/types/attendance.ts
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day'; export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
export interface AttendanceRecord { export interface AttendanceRecord {
@ -61,4 +60,3 @@ export interface AdminReportRecord {
lateArrivals: number; lateArrivals: number;
}; };
} }
// Note: RegularizationRecord is defined in useAttendanceData.ts to keep types close to the hook.

View File

@ -11,9 +11,9 @@ const MOCK_ROLE_MAP: Record<UserRole, string> = {
}; };
export const MOCK_USERS: Record<UserRole, { userId: string; branches: string }> = { export const MOCK_USERS: Record<UserRole, { userId: string; branches: string }> = {
employee: { userId: '126', branches: '1' }, employee: { userId: '135', branches: '1' },
manager: { userId: '127', branches: '1' }, manager: { userId: '150', branches: '1' },
hrmanager: { userId: '135', branches: '1,2' }, hrmanager: { userId: '126', branches: '1,2' },
director: { userId: '113', branches: '1,2,3' } director: { userId: '113', branches: '1,2,3' }
}; };
@ -25,10 +25,10 @@ export interface RoleState {
} }
const initialState: RoleState = { const initialState: RoleState = {
currentRole: 'employee', // Default to employee currentRole: 'hrmanager', // Default to hrmanager
mockUserId: MOCK_USERS.employee.userId, mockUserId: MOCK_USERS.hrmanager.userId,
mockUserRole: MOCK_ROLE_MAP.employee, mockUserRole: MOCK_ROLE_MAP.hrmanager,
mockBranches: MOCK_USERS.employee.branches, mockBranches: MOCK_USERS.hrmanager.branches,
}; };
const roleSlice = createSlice({ const roleSlice = createSlice({