feat: add payroll, time clocks, and leave request modules
- Implemented PayrollPage for managing payroll runs with CRUD operations. - Created TimeClocksPage for tracking employee clock-ins and clock-outs. - Developed TimeSheetsPage for displaying employee timesheets. - Added EmployeeCertificationForm and EmployeeDocumentForm for managing employee certifications and documents. - Introduced LeaveRequestForm for submitting and managing leave requests. - Added usePermissions hook for UI-side permission checks. - Created LeaveRequestsClient and PayrollClient for API interactions.
This commit is contained in:
@@ -59,6 +59,8 @@ import { ExpenseItemsClient } from "./clients/expense-items"
|
||||
import { InventoryCategoriesClient } from "./clients/inventory-categories"
|
||||
import { DocumentPrintClient } from "./clients/document-print"
|
||||
import { DocumentShareClient } from "./clients/document-share"
|
||||
import { LeaveRequestsClient } from "./clients/leave-requests"
|
||||
import { PayrollClient } from "./clients/payroll"
|
||||
|
||||
export function createApi(options?: ApiClientOptions) {
|
||||
return {
|
||||
@@ -122,6 +124,8 @@ export function createApi(options?: ApiClientOptions) {
|
||||
inventoryCategories: new InventoryCategoriesClient(undefined, options),
|
||||
documentPrint: new DocumentPrintClient(undefined, options),
|
||||
documentShare: new DocumentShareClient(undefined, options),
|
||||
leaveRequests: new LeaveRequestsClient(undefined, options),
|
||||
payroll: new PayrollClient(undefined, options),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CrudClient } from "../infra/crud-client"
|
||||
import type { ApiClientOptions } from "../infra/client"
|
||||
import type { ApiPath, ApiRequestBody } from "../infra/types"
|
||||
import type { ApiListQueryParams } from "../contracts/types"
|
||||
|
||||
export const EMPLOYEE_ROUTES = {
|
||||
INDEX: "/api/employees",
|
||||
@@ -8,12 +9,36 @@ export const EMPLOYEE_ROUTES = {
|
||||
UPDATE_PERMISSIONS: "/api/employees/{id}/update-permissions",
|
||||
} as const satisfies Record<string, ApiPath>
|
||||
|
||||
// Routes added after the last OpenAPI regen — not yet in the generated `paths`
|
||||
// type, so callers go through `as never` casts.
|
||||
export const EMPLOYEE_EXTRA_ROUTES = {
|
||||
UPLOAD_AVATAR: "/api/employees/{id}/upload-avatar",
|
||||
PERFORMANCE: "/api/employees/{id}/performance",
|
||||
IMPORT: "/api/employees/import",
|
||||
EXPORT: "/api/employees/export",
|
||||
IMPORT_SAMPLE: "/api/employees/import/sample",
|
||||
DOCUMENTS: "/api/employees/{employee}/documents",
|
||||
DOCUMENT_BY_ID: "/api/employees/{employee}/documents/{id}",
|
||||
CERTIFICATIONS: "/api/employees/{employee}/certifications",
|
||||
CERTIFICATION_BY_ID: "/api/employees/{employee}/certifications/{id}",
|
||||
LEAVE_BALANCE: "/api/employees/{employee}/leave-balance",
|
||||
PAYROLL_SLIPS: "/api/employees/{employee}/payroll-slips",
|
||||
} as const
|
||||
|
||||
export class EmployeesClient extends CrudClient<
|
||||
typeof EMPLOYEE_ROUTES.INDEX,
|
||||
typeof EMPLOYEE_ROUTES.BY_ID
|
||||
> {
|
||||
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
|
||||
super(baseUrl, defaultOptions, EMPLOYEE_ROUTES.INDEX, EMPLOYEE_ROUTES.BY_ID)
|
||||
super(
|
||||
baseUrl,
|
||||
defaultOptions,
|
||||
EMPLOYEE_ROUTES.INDEX,
|
||||
EMPLOYEE_ROUTES.BY_ID,
|
||||
EMPLOYEE_EXTRA_ROUTES.IMPORT as never,
|
||||
EMPLOYEE_EXTRA_ROUTES.EXPORT as never,
|
||||
"GET",
|
||||
)
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
@@ -23,4 +48,118 @@ export class EmployeesClient extends CrudClient<
|
||||
async updatePermissions(id: string, payload: ApiRequestBody<typeof EMPLOYEE_ROUTES.UPDATE_PERMISSIONS, "post">) {
|
||||
return this.post(EMPLOYEE_ROUTES.UPDATE_PERMISSIONS, payload as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async uploadAvatar(id: string, file: File) {
|
||||
const fd = new FormData()
|
||||
fd.append("avatar", file)
|
||||
return this.postFormData(`/api/employees/${id}/upload-avatar`, fd)
|
||||
}
|
||||
|
||||
async downloadImportSample() {
|
||||
return this.fetchBlob(EMPLOYEE_EXTRA_ROUTES.IMPORT_SAMPLE as never, { method: "GET" })
|
||||
}
|
||||
|
||||
async performance(id: string, query?: { from?: string; to?: string }) {
|
||||
return this.get(EMPLOYEE_EXTRA_ROUTES.PERFORMANCE as never, { params: { id }, query } as never)
|
||||
}
|
||||
|
||||
// ── Documents ────────────────────────────────────────────────────
|
||||
async listDocuments(employeeId: string, query?: ApiListQueryParams) {
|
||||
return this.get(EMPLOYEE_EXTRA_ROUTES.DOCUMENTS as never, { params: { employee: employeeId }, query } as never)
|
||||
}
|
||||
|
||||
async uploadDocument(employeeId: string, payload: {
|
||||
file: File
|
||||
name: string
|
||||
type: string
|
||||
issued_date?: string
|
||||
expiry_date?: string
|
||||
notes?: string
|
||||
}) {
|
||||
const fd = new FormData()
|
||||
fd.append("file", payload.file)
|
||||
fd.append("name", payload.name)
|
||||
fd.append("type", payload.type)
|
||||
if (payload.issued_date) fd.append("issued_date", payload.issued_date)
|
||||
if (payload.expiry_date) fd.append("expiry_date", payload.expiry_date)
|
||||
if (payload.notes) fd.append("notes", payload.notes)
|
||||
return this.postFormData(`/api/employees/${employeeId}/documents`, fd)
|
||||
}
|
||||
|
||||
async updateDocument(employeeId: string, id: string, payload: Record<string, unknown>) {
|
||||
return this.put(EMPLOYEE_EXTRA_ROUTES.DOCUMENT_BY_ID as never, payload as never, { params: { employee: employeeId, id } } as never)
|
||||
}
|
||||
|
||||
async destroyDocument(employeeId: string, id: string) {
|
||||
return this.delete(EMPLOYEE_EXTRA_ROUTES.DOCUMENT_BY_ID as never, { params: { employee: employeeId, id } } as never)
|
||||
}
|
||||
|
||||
// ── Certifications ───────────────────────────────────────────────
|
||||
async listCertifications(employeeId: string, query?: ApiListQueryParams) {
|
||||
return this.get(EMPLOYEE_EXTRA_ROUTES.CERTIFICATIONS as never, { params: { employee: employeeId }, query } as never)
|
||||
}
|
||||
|
||||
async createCertification(employeeId: string, payload: {
|
||||
name: string
|
||||
issuer?: string
|
||||
certificate_number?: string
|
||||
issued_date?: string
|
||||
expiry_date?: string
|
||||
notes?: string
|
||||
file?: File
|
||||
}) {
|
||||
const fd = new FormData()
|
||||
fd.append("name", payload.name)
|
||||
if (payload.issuer) fd.append("issuer", payload.issuer)
|
||||
if (payload.certificate_number) fd.append("certificate_number", payload.certificate_number)
|
||||
if (payload.issued_date) fd.append("issued_date", payload.issued_date)
|
||||
if (payload.expiry_date) fd.append("expiry_date", payload.expiry_date)
|
||||
if (payload.notes) fd.append("notes", payload.notes)
|
||||
if (payload.file) fd.append("file", payload.file)
|
||||
return this.postFormData(`/api/employees/${employeeId}/certifications`, fd)
|
||||
}
|
||||
|
||||
async updateCertification(employeeId: string, id: string, payload: {
|
||||
name?: string
|
||||
issuer?: string
|
||||
certificate_number?: string
|
||||
issued_date?: string
|
||||
expiry_date?: string
|
||||
notes?: string
|
||||
file?: File
|
||||
}) {
|
||||
const fd = new FormData()
|
||||
if (payload.name) fd.append("name", payload.name)
|
||||
if (payload.issuer) fd.append("issuer", payload.issuer)
|
||||
if (payload.certificate_number) fd.append("certificate_number", payload.certificate_number)
|
||||
if (payload.issued_date) fd.append("issued_date", payload.issued_date)
|
||||
if (payload.expiry_date) fd.append("expiry_date", payload.expiry_date)
|
||||
if (payload.notes) fd.append("notes", payload.notes)
|
||||
if (payload.file) fd.append("file", payload.file)
|
||||
return this.postFormData(`/api/employees/${employeeId}/certifications/${id}`, fd)
|
||||
}
|
||||
|
||||
async destroyCertification(employeeId: string, id: string) {
|
||||
return this.delete(EMPLOYEE_EXTRA_ROUTES.CERTIFICATION_BY_ID as never, { params: { employee: employeeId, id } } as never)
|
||||
}
|
||||
|
||||
// ── Leave balance ────────────────────────────────────────────────
|
||||
async getLeaveBalance(employeeId: string, year?: number) {
|
||||
return this.get(EMPLOYEE_EXTRA_ROUTES.LEAVE_BALANCE as never, {
|
||||
params: { employee: employeeId },
|
||||
query: year ? { year } : undefined,
|
||||
} as never)
|
||||
}
|
||||
|
||||
async updateLeaveBalance(employeeId: string, payload: { year: number; annual_total?: number; sick_total?: number }) {
|
||||
return this.put(EMPLOYEE_EXTRA_ROUTES.LEAVE_BALANCE as never, payload as never, { params: { employee: employeeId } } as never)
|
||||
}
|
||||
|
||||
// ── Payroll slips for an employee ────────────────────────────────
|
||||
async listPayrollSlips(employeeId: string, query?: ApiListQueryParams & { year?: number }) {
|
||||
return this.get(EMPLOYEE_EXTRA_ROUTES.PAYROLL_SLIPS as never, {
|
||||
params: { employee: employeeId },
|
||||
query,
|
||||
} as never)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +83,5 @@ export { ConfigurationsClient, CONFIGURATION_ROUTES } from "./configurations"
|
||||
export { AutoGenerateClient, AUTO_GENERATE_ROUTES } from "./auto-generate"
|
||||
export { ExpenseItemsClient, EXPENSE_ITEM_ROUTES } from "./expense-items"
|
||||
export { InventoryCategoriesClient, INVENTORY_CATEGORY_ROUTES } from "./inventory-categories"
|
||||
export { LeaveRequestsClient, LEAVE_REQUEST_ROUTES } from "./leave-requests"
|
||||
export { PayrollClient, PAYROLL_ROUTES } from "./payroll"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiClient, type ApiClientOptions } from "../infra/client"
|
||||
import type { ApiListQueryParams } from "../contracts/types"
|
||||
|
||||
// New endpoints — not yet in the OpenAPI spec; routes are raw strings cast as needed.
|
||||
export const LEAVE_REQUEST_ROUTES = {
|
||||
INDEX: "/api/leave-requests",
|
||||
BY_ID: "/api/leave-requests/{id}",
|
||||
APPROVE: "/api/leave-requests/{id}/approve",
|
||||
REJECT: "/api/leave-requests/{id}/reject",
|
||||
} as const
|
||||
|
||||
export type LeaveStatus = "pending" | "approved" | "rejected" | "cancelled"
|
||||
|
||||
export class LeaveRequestsClient extends ApiClient {
|
||||
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
|
||||
super(baseUrl, defaultOptions)
|
||||
}
|
||||
|
||||
async list(query?: ApiListQueryParams & { employee_id?: string | number; status?: LeaveStatus; leave_type?: string; year?: number }) {
|
||||
return this.get(LEAVE_REQUEST_ROUTES.INDEX as never, { query } as never)
|
||||
}
|
||||
|
||||
async show(id: string) {
|
||||
return this.get(LEAVE_REQUEST_ROUTES.BY_ID as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return this.show(id)
|
||||
}
|
||||
|
||||
async create(payload: { employee_id: number | string; leave_type: string; start_date: string; end_date: string; reason?: string }) {
|
||||
return this.post(LEAVE_REQUEST_ROUTES.INDEX as never, payload as never)
|
||||
}
|
||||
|
||||
async update(id: string, payload: { leave_type?: string; start_date?: string; end_date?: string; reason?: string }) {
|
||||
return this.put(LEAVE_REQUEST_ROUTES.BY_ID as never, payload as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async destroy(id: string) {
|
||||
return this.delete(LEAVE_REQUEST_ROUTES.BY_ID as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async approve(id: string, payload?: { decision_note?: string; approved_by?: number }) {
|
||||
return this.post(LEAVE_REQUEST_ROUTES.APPROVE as never, (payload ?? {}) as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async reject(id: string, payload?: { decision_note?: string; approved_by?: number }) {
|
||||
return this.post(LEAVE_REQUEST_ROUTES.REJECT as never, (payload ?? {}) as never, { params: { id } } as never)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ApiClient, type ApiClientOptions } from "../infra/client"
|
||||
import type { ApiListQueryParams } from "../contracts/types"
|
||||
|
||||
export const PAYROLL_ROUTES = {
|
||||
RUNS_INDEX: "/api/payroll/runs",
|
||||
RUN_BY_ID: "/api/payroll/runs/{id}",
|
||||
REGENERATE: "/api/payroll/runs/{id}/regenerate",
|
||||
FINALIZE: "/api/payroll/runs/{id}/finalize",
|
||||
MARK_PAID: "/api/payroll/runs/{id}/mark-paid",
|
||||
ENTRY_BY_ID: "/api/payroll/runs/{run}/entries/{entry}",
|
||||
} as const
|
||||
|
||||
export type PayrollStatus = "draft" | "finalized" | "paid"
|
||||
|
||||
export class PayrollClient extends ApiClient {
|
||||
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
|
||||
super(baseUrl, defaultOptions)
|
||||
}
|
||||
|
||||
async list(query?: ApiListQueryParams & { status?: PayrollStatus; year?: number }) {
|
||||
return this.get(PAYROLL_ROUTES.RUNS_INDEX as never, { query } as never)
|
||||
}
|
||||
|
||||
async show(id: string) {
|
||||
return this.get(PAYROLL_ROUTES.RUN_BY_ID as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return this.show(id)
|
||||
}
|
||||
|
||||
async create(payload: { period_start: string; period_end: string; reference?: string; note?: string }) {
|
||||
return this.post(PAYROLL_ROUTES.RUNS_INDEX as never, payload as never)
|
||||
}
|
||||
|
||||
async destroy(id: string) {
|
||||
return this.delete(PAYROLL_ROUTES.RUN_BY_ID as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async regenerate(id: string) {
|
||||
return this.post(PAYROLL_ROUTES.REGENERATE as never, {} as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async finalize(id: string) {
|
||||
return this.post(PAYROLL_ROUTES.FINALIZE as never, {} as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async markPaid(id: string) {
|
||||
return this.post(PAYROLL_ROUTES.MARK_PAID as never, {} as never, { params: { id } } as never)
|
||||
}
|
||||
|
||||
async updateEntry(runId: string, entryId: string, payload: {
|
||||
hours_worked?: number
|
||||
overtime_hours?: number
|
||||
base_salary?: number
|
||||
commission_amount?: number
|
||||
allowances?: number
|
||||
deductions?: number
|
||||
note?: string
|
||||
}) {
|
||||
return this.put(PAYROLL_ROUTES.ENTRY_BY_ID as never, payload as never, { params: { run: runId, entry: entryId } } as never)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user