Compare commits

..

No commits in common. "c561ab7b888b1578aec311aa88460fdbec593daf" and "358aa9f7ca1ceb79aa483a31f421f3df92aa93fb" have entirely different histories.

9 changed files with 68 additions and 20 deletions

View File

@ -13,7 +13,7 @@ import {
const iconMap: Record<string, any> = {
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus
Database, Building, Network, Briefcase, Tag, ClipboardType,ClipboardMinus
};
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
@ -50,6 +50,7 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
<li key={item.name} className="mb-1 relative group">
{hasChildren ? (
<button
// Toggle pin on click
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'
}`}
@ -66,6 +67,8 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
</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
className={`${isPinned ? 'block' : 'hidden'} group-hover:block absolute left-full ml-4 top-0 z-50 shadow-lg`}
>
@ -134,12 +137,8 @@ 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={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
{!isCollapsed && (
<Link to="/dashboard" className="flex items-center flex-1 overflow-hidden">
<img
src="/logo.gif"
alt="Company Logo"
className="h-14 w-5xl max-w-75 cursor-pointer "
/>
<Link to="/dashboard" className="flex items-center flex-shrink-0">
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" />
</Link>
)}
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors flex-shrink-0">
@ -147,6 +146,11 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
</button>
</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'}`}>
<ul>
{navItems.map(item => renderNavItem(item))}

View File

@ -33,6 +33,7 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
];
}
// Shared children for HRMANAGER and DIRECTOR
const attendanceSummaryItems: NavItem = {
name: 'Attendance Service',
icon: 'BarChart3',
@ -69,6 +70,7 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
]
};
// 3. HRMANAGER
if (role === 'hrmanager') {
return [
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
@ -79,6 +81,7 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
];
}
// 4. DIRECTOR
if (role === 'director') {
return [
{ 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 { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
let newFilters = { ...filters, [name]: value };
// Reset child dropdowns if parent changes
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }

View File

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

View File

@ -5,6 +5,8 @@ import ErrorState from '../../../components/ui/ErrorState';
import RegularizationTable from '../components/summary/RegularizationTable';
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') => {
return records.map(r => r.id === id ? { ...r, status: newStatus } : r);
};
@ -16,7 +18,6 @@ export default function HRManagerRegularization() {
// Once data is loaded, sync it to local state so we can update statuses without refetching immediately
useMemo(() => {
if (initialRecords) {
// eslint-disable-next-line react-hooks/set-state-in-render
setLocalRecords(initialRecords);
}
}, [initialRecords]);

View File

@ -26,6 +26,7 @@ export default function MonthlyReport() {
employeeCode: '',
});
// Fetch data using the hook
const { data: reportData, isLoading, isFetching } = useAdminReport(filters);
const handleDownload = () => {
@ -56,6 +57,7 @@ export default function MonthlyReport() {
<MonthlyReportFilters filters={filters} onFilterChange={setFilters} />
{/* Data Table */}
<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">
Filtered Results ({reportData?.length || 0})

View File

@ -1,5 +1,40 @@
import MyAttendance from '../pages/MyAttendance';
export default function AttendanceRouter() {
// Since MyAttendance pulls the ID dynamically from Redux,
// it works perfectly for Employee, Manager, HR, and Director.
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,3 +1,4 @@
// src/features/attendance/types/attendance.ts
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
export interface AttendanceRecord {
@ -60,3 +61,4 @@ export interface AdminReportRecord {
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 }> = {
employee: { userId: '135', branches: '1' },
manager: { userId: '150', branches: '1' },
hrmanager: { userId: '126', branches: '1,2' },
employee: { userId: '126', branches: '1' },
manager: { userId: '127', branches: '1' },
hrmanager: { userId: '135', branches: '1,2' },
director: { userId: '113', branches: '1,2,3' }
};
@ -25,10 +25,10 @@ export interface RoleState {
}
const initialState: RoleState = {
currentRole: 'hrmanager', // Default to hrmanager
mockUserId: MOCK_USERS.hrmanager.userId,
mockUserRole: MOCK_ROLE_MAP.hrmanager,
mockBranches: MOCK_USERS.hrmanager.branches,
currentRole: 'employee', // Default to employee
mockUserId: MOCK_USERS.employee.userId,
mockUserRole: MOCK_ROLE_MAP.employee,
mockBranches: MOCK_USERS.employee.branches,
};
const roleSlice = createSlice({