Compare commits
No commits in common. "c561ab7b888b1578aec311aa88460fdbec593daf" and "358aa9f7ca1ceb79aa483a31f421f3df92aa93fb" have entirely different histories.
c561ab7b88
...
358aa9f7ca
@ -50,6 +50,7 @@ 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'
|
||||||
}`}
|
}`}
|
||||||
@ -66,6 +67,8 @@ 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`}
|
||||||
>
|
>
|
||||||
@ -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={`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-1 overflow-hidden">
|
<Link to="/dashboard" className="flex items-center flex-shrink-0">
|
||||||
<img
|
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" />
|
||||||
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">
|
||||||
@ -147,6 +146,11 @@ 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))}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ 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',
|
||||||
@ -69,6 +70,7 @@ 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' },
|
||||||
@ -79,6 +81,7 @@ 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' },
|
||||||
|
|||||||
@ -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;
|
||||||
const newFilters = { ...filters, [name]: value };
|
let 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 = ''; }
|
||||||
|
|||||||
@ -20,6 +20,7 @@ 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 = () => {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ 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);
|
||||||
};
|
};
|
||||||
@ -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
|
// 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]);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ 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 = () => {
|
||||||
@ -56,6 +57,7 @@ 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})
|
||||||
|
|||||||
@ -1,5 +1,40 @@
|
|||||||
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!
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// 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 {
|
||||||
@ -60,3 +61,4 @@ export interface AdminReportRecord {
|
|||||||
lateArrivals: number;
|
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 }> = {
|
export const MOCK_USERS: Record<UserRole, { userId: string; branches: string }> = {
|
||||||
employee: { userId: '135', branches: '1' },
|
employee: { userId: '126', branches: '1' },
|
||||||
manager: { userId: '150', branches: '1' },
|
manager: { userId: '127', branches: '1' },
|
||||||
hrmanager: { userId: '126', branches: '1,2' },
|
hrmanager: { userId: '135', 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: 'hrmanager', // Default to hrmanager
|
currentRole: 'employee', // Default to employee
|
||||||
mockUserId: MOCK_USERS.hrmanager.userId,
|
mockUserId: MOCK_USERS.employee.userId,
|
||||||
mockUserRole: MOCK_ROLE_MAP.hrmanager,
|
mockUserRole: MOCK_ROLE_MAP.employee,
|
||||||
mockBranches: MOCK_USERS.hrmanager.branches,
|
mockBranches: MOCK_USERS.employee.branches,
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleSlice = createSlice({
|
const roleSlice = createSlice({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user