Compare commits
2 Commits
358aa9f7ca
...
c561ab7b88
| Author | SHA1 | Date | |
|---|---|---|---|
| c561ab7b88 | |||
| c1b7eba8af |
@ -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,7 +50,6 @@ 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'
|
||||
}`}
|
||||
@ -67,8 +66,6 @@ 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`}
|
||||
>
|
||||
@ -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={`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-shrink-0">
|
||||
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" />
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</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))}
|
||||
|
||||
@ -33,7 +33,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
];
|
||||
}
|
||||
|
||||
// Shared children for HRMANAGER and DIRECTOR
|
||||
const attendanceSummaryItems: NavItem = {
|
||||
name: 'Attendance Service',
|
||||
icon: 'BarChart3',
|
||||
@ -70,7 +69,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
]
|
||||
};
|
||||
|
||||
// 3. HRMANAGER
|
||||
if (role === 'hrmanager') {
|
||||
return [
|
||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||
@ -81,7 +79,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
];
|
||||
}
|
||||
|
||||
// 4. DIRECTOR
|
||||
if (role === 'director') {
|
||||
return [
|
||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||
|
||||
@ -33,7 +33,7 @@ export default function MonthlyReportFilters({ filters, onFilterChange }: Monthl
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
let newFilters = { ...filters, [name]: value };
|
||||
const newFilters = { ...filters, [name]: value };
|
||||
|
||||
// Reset child dropdowns if parent changes
|
||||
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }
|
||||
|
||||
@ -20,7 +20,6 @@ export default function DailyReport() {
|
||||
employeeCode: '',
|
||||
});
|
||||
|
||||
// Fetch data using the hook
|
||||
const { data: reportData, isLoading, isFetching } = useDailyReport(filters);
|
||||
|
||||
const handleDownload = () => {
|
||||
|
||||
@ -5,8 +5,6 @@ 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);
|
||||
};
|
||||
@ -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
|
||||
useMemo(() => {
|
||||
if (initialRecords) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-render
|
||||
setLocalRecords(initialRecords);
|
||||
}
|
||||
}, [initialRecords]);
|
||||
|
||||
@ -26,7 +26,6 @@ export default function MonthlyReport() {
|
||||
employeeCode: '',
|
||||
});
|
||||
|
||||
// Fetch data using the hook
|
||||
const { data: reportData, isLoading, isFetching } = useAdminReport(filters);
|
||||
|
||||
const handleDownload = () => {
|
||||
@ -57,7 +56,6 @@ 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})
|
||||
|
||||
@ -1,40 +1,5 @@
|
||||
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!
|
||||
@ -1,4 +1,3 @@
|
||||
// src/features/attendance/types/attendance.ts
|
||||
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
||||
|
||||
export interface AttendanceRecord {
|
||||
@ -61,4 +60,3 @@ export interface AdminReportRecord {
|
||||
lateArrivals: number;
|
||||
};
|
||||
}
|
||||
// Note: RegularizationRecord is defined in useAttendanceData.ts to keep types close to the hook.
|
||||
@ -11,9 +11,9 @@ const MOCK_ROLE_MAP: Record<UserRole, string> = {
|
||||
};
|
||||
|
||||
export const MOCK_USERS: Record<UserRole, { userId: string; branches: string }> = {
|
||||
employee: { userId: '126', branches: '1' },
|
||||
manager: { userId: '127', branches: '1' },
|
||||
hrmanager: { userId: '135', branches: '1,2' },
|
||||
employee: { userId: '135', branches: '1' },
|
||||
manager: { userId: '150', branches: '1' },
|
||||
hrmanager: { userId: '126', branches: '1,2' },
|
||||
director: { userId: '113', branches: '1,2,3' }
|
||||
};
|
||||
|
||||
@ -25,10 +25,10 @@ export interface RoleState {
|
||||
}
|
||||
|
||||
const initialState: RoleState = {
|
||||
currentRole: 'employee', // Default to employee
|
||||
mockUserId: MOCK_USERS.employee.userId,
|
||||
mockUserRole: MOCK_ROLE_MAP.employee,
|
||||
mockBranches: MOCK_USERS.employee.branches,
|
||||
currentRole: 'hrmanager', // Default to hrmanager
|
||||
mockUserId: MOCK_USERS.hrmanager.userId,
|
||||
mockUserRole: MOCK_ROLE_MAP.hrmanager,
|
||||
mockBranches: MOCK_USERS.hrmanager.branches,
|
||||
};
|
||||
|
||||
const roleSlice = createSlice({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user