87e9346d62
- NestJS backend with 11 modules: Auth, Employees, Departments, Attendance, Leaves, Payroll, Reimbursements, Announcements, Tax, Reports, Admin - JWT authentication with refresh tokens, role-based access (employee/hr_admin/super_admin) - MongoDB schemas with Mongoose for all entities - PDF payslip generation with pdfkit - OpenTelemetry tracing to SigNoz - Automatic database seeding on first startup - Next.js 14 frontend with App Router, Tailwind CSS - 25 pages covering all employee, HR admin, and super admin workflows - Multi-stage Dockerfile with nginx proxy - test-manifest.json for E2E testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
181 lines
6.2 KiB
TypeScript
181 lines
6.2 KiB
TypeScript
import axios from 'axios';
|
|
|
|
const BASE_URL = (process.env.NEXT_PUBLIC_API_URL || '/api-backend') + '/api/v1';
|
|
|
|
const api = axios.create({
|
|
baseURL: BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor to attach JWT
|
|
api.interceptors.request.use(
|
|
(config) => {
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
}
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error),
|
|
);
|
|
|
|
// Response interceptor to handle 401 and refresh
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
const originalRequest = error.config;
|
|
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
originalRequest._retry = true;
|
|
|
|
if (typeof window !== 'undefined') {
|
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
if (refreshToken) {
|
|
try {
|
|
const response = await axios.post(`${BASE_URL}/auth/refresh`, {
|
|
refreshToken,
|
|
});
|
|
const { accessToken } = response.data;
|
|
localStorage.setItem('access_token', accessToken);
|
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
|
return api(originalRequest);
|
|
} catch (refreshError) {
|
|
// Refresh failed, clear tokens and redirect to login
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
localStorage.removeItem('user');
|
|
if (typeof window !== 'undefined') {
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
export default api;
|
|
|
|
// Auth
|
|
export const authApi = {
|
|
login: (employeeId: string, password: string) =>
|
|
api.post('/auth/login', { employeeId, password }),
|
|
logout: () => api.post('/auth/logout'),
|
|
changePassword: (currentPassword: string, newPassword: string) =>
|
|
api.post('/auth/change-password', { currentPassword, newPassword }),
|
|
getMe: () => api.get('/auth/me'),
|
|
};
|
|
|
|
// Employees
|
|
export const employeesApi = {
|
|
getAll: (params?: any) => api.get('/employees', { params }),
|
|
getOne: (id: string) => api.get(`/employees/${id}`),
|
|
create: (data: any) => api.post('/employees', data),
|
|
update: (id: string, data: any) => api.put(`/employees/${id}`, data),
|
|
delete: (id: string) => api.delete(`/employees/${id}`),
|
|
importCsv: (file: File) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return api.post('/employees/import-csv', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
},
|
|
};
|
|
|
|
// Departments
|
|
export const departmentsApi = {
|
|
getAll: () => api.get('/departments'),
|
|
create: (data: any) => api.post('/departments', data),
|
|
update: (id: string, data: any) => api.put(`/departments/${id}`, data),
|
|
delete: (id: string) => api.delete(`/departments/${id}`),
|
|
};
|
|
|
|
// Attendance
|
|
export const attendanceApi = {
|
|
mark: (data?: any) => api.post('/attendance', data || {}),
|
|
update: (id: string, data: any) => api.put(`/attendance/${id}`, data),
|
|
getAll: (params?: any) => api.get('/attendance', { params }),
|
|
};
|
|
|
|
// Leaves
|
|
export const leavesApi = {
|
|
apply: (data: any) => api.post('/leaves', data),
|
|
getAll: (params?: any) => api.get('/leaves', { params }),
|
|
approve: (id: string, comment?: string) =>
|
|
api.put(`/leaves/${id}/approve`, { comment }),
|
|
reject: (id: string, comment: string) =>
|
|
api.put(`/leaves/${id}/reject`, { comment }),
|
|
getBalance: (employeeId: string) => api.get(`/leaves/balance/${employeeId}`),
|
|
};
|
|
|
|
// Payroll
|
|
export const payrollApi = {
|
|
generate: (month: number, year: number) =>
|
|
api.post('/payroll/generate', { month, year }),
|
|
getPayrollRuns: (params?: any) => api.get('/payroll', { params }),
|
|
getPayslips: (params?: any) => api.get('/payslips', { params }),
|
|
getPayslip: (id: string) => api.get(`/payslips/${id}`),
|
|
downloadPdf: (id: string) =>
|
|
api.get(`/payslips/${id}/pdf`, { responseType: 'blob' }),
|
|
};
|
|
|
|
// Reimbursements
|
|
export const reimbursementsApi = {
|
|
create: (data: any, receipt?: File) => {
|
|
const formData = new FormData();
|
|
Object.keys(data).forEach((key) => formData.append(key, data[key]));
|
|
if (receipt) formData.append('receipt', receipt);
|
|
return api.post('/reimbursements', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
},
|
|
getAll: (params?: any) => api.get('/reimbursements', { params }),
|
|
approve: (id: string, comment?: string) =>
|
|
api.put(`/reimbursements/${id}/approve`, { comment }),
|
|
reject: (id: string, comment: string) =>
|
|
api.put(`/reimbursements/${id}/reject`, { comment }),
|
|
markPaid: (id: string) => api.put(`/reimbursements/${id}/mark-paid`),
|
|
};
|
|
|
|
// Announcements
|
|
export const announcementsApi = {
|
|
getAll: () => api.get('/announcements'),
|
|
create: (data: any) => api.post('/announcements', data),
|
|
update: (id: string, data: any) => api.put(`/announcements/${id}`, data),
|
|
delete: (id: string) => api.delete(`/announcements/${id}`),
|
|
};
|
|
|
|
// Tax
|
|
export const taxApi = {
|
|
submitDeclaration: (data: any) => api.post('/tax/declarations', data),
|
|
getDeclaration: (employeeId: string) => api.get(`/tax/declarations/${employeeId}`),
|
|
getTDSProjection: (employeeId: string) => api.get(`/tax/tds-projection/${employeeId}`),
|
|
};
|
|
|
|
// Reports
|
|
export const reportsApi = {
|
|
getHeadcount: () => api.get('/reports/headcount'),
|
|
getPayrollSummary: (month: number, year: number) =>
|
|
api.get('/reports/payroll-summary', { params: { month, year } }),
|
|
getLeaveUtilization: (year: number) =>
|
|
api.get('/reports/leave-utilization', { params: { year } }),
|
|
getAttendanceSummary: (month: number, year: number) =>
|
|
api.get('/reports/attendance-summary', { params: { month, year } }),
|
|
};
|
|
|
|
// Admin
|
|
export const adminApi = {
|
|
getAccounts: () => api.get('/admin/accounts'),
|
|
createAccount: (data: any) => api.post('/admin/accounts', data),
|
|
deactivateAccount: (id: string) => api.put(`/admin/accounts/${id}/deactivate`),
|
|
getAuditLogs: (params?: any) => api.get('/audit-logs', { params }),
|
|
getOrgSettings: () => api.get('/org/settings'),
|
|
updateOrgSettings: (data: any) => api.put('/org/settings', data),
|
|
};
|