wages/frontend/src/api.ts
jtricerolph 5372111f52 Add per-employee dept breakdown via modal popup
Stores per-employee shift costs in wage_actuals_detail (APPROVED shifts
only). Sync fetches employee name map from /api/v2/users alongside dept
names. Clicking any dept row in Weekly or Monthly opens a modal showing
Employee · Shifts · Cost · % of Dept, fetched on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 11:02:11 +00:00

84 lines
3.1 KiB
TypeScript

import type { DeptActuals, DeptScheduled, NetSalesDay, WageBudget, AppSetting, DeptPct, EmployeeDetail } from './types'
const BASE = '/wages/api'
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
const headers: Record<string, string> = opts.body != null ? { 'Content-Type': 'application/json' } : {}
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
headers: { ...headers, ...opts.headers },
...opts,
})
if (res.status === 401) {
;(window.top ?? window).location.href = '/login'
throw new Error('Unauthenticated')
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error((err as { error?: string }).error || `Request failed: ${res.status}`)
}
return res.json()
}
export function getActuals(from: string, to: string): Promise<{ departments: DeptActuals[]; show_oncosts: boolean; dept_pcts: Record<string, number> }> {
return request(`/actuals?from=${from}&to=${to}`)
}
export function getScheduled(from: string, to: string): Promise<{ departments: DeptScheduled[] }> {
return request(`/scheduled?from=${from}&to=${to}`)
}
export function getNetSales(from: string, to: string): Promise<{ days: NetSalesDay[] }> {
return request(`/net-sales?from=${from}&to=${to}`)
}
export function getBudgets(): Promise<{ budgets: WageBudget[] }> {
return request('/budgets')
}
export function saveBudget(month: string, budget_amount: number): Promise<{ ok: boolean }> {
return request(`/budgets/${month}`, {
method: 'PUT',
body: JSON.stringify({ budget_amount }),
})
}
export function triggerSync(): Promise<{ ok: boolean; actual_rows: number; scheduled_rows: number }> {
return request('/sync', { method: 'POST' })
}
export function getSyncStatus(): Promise<{ sync_last_at: string | null; backfill_last_at: string | null; backfill_running: boolean }> {
return request('/sync/status')
}
export function cancelBackfill(): Promise<{ ok: boolean }> {
return request('/sync/backfill/cancel', { method: 'POST' })
}
export function getDepartments(): Promise<{ departments: { id: string; name: string }[] }> {
return request('/departments')
}
export function getSettings(): Promise<{ settings: AppSetting[] }> {
return request('/settings')
}
export function saveSettings(settings: { key: string; value: string }[]): Promise<{ ok: boolean }> {
return request('/settings', { method: 'PUT', body: JSON.stringify({ settings }) })
}
export function getDeptConfig(): Promise<{ depts: DeptPct[] }> {
return request('/budgets/dept-config')
}
export function saveDeptPcts(pcts: { id: string; pct: number }[]): Promise<{ ok: boolean }> {
return request('/budgets/dept-pcts', { method: 'PUT', body: JSON.stringify({ pcts }) })
}
export function getDeptDetail(deptId: string, from: string, to: string): Promise<{ employees: EmployeeDetail[] }> {
return request(`/actuals/dept-detail?dept_id=${encodeURIComponent(deptId)}&from=${from}&to=${to}`)
}
export function downloadExport(view: string, from: string, to: string): void {
window.open(`${BASE}/export?view=${view}&from=${from}&to=${to}`, '_blank')
}