import type { Task, TaskDetail, TaskStatus, Location, Category, Asset, AssetDetail, Contractor, ContractorDetail, ContractorDoc, Template, AppConfig, AuthUser, TaskPhoto, } from './types' const BASE = '/maintenance/api' async function request(path: string, opts: RequestInit = {}): Promise { const res = await fetch(`${BASE}${path}`, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...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.error || `Request failed: ${res.status}`) } return res.json() } // Locations export function fetchLocations(): Promise<{ categories: Category[]; locations: Location[] }> { return request('/locations') } export function createLocation(body: { name: string; category_id: number }): Promise { return request('/locations', { method: 'POST', body: JSON.stringify(body) }) } export function updateLocation(id: number, body: Partial): Promise { return request(`/locations/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function syncNewbookRooms(): Promise<{ ok: boolean; created: number; updated: number; total: number }> { return request('/locations/sync-newbook', { method: 'POST', body: JSON.stringify({}) }) } export function createCategory(body: { name: string; sort_order?: number; is_rooms?: boolean }): Promise { return request('/categories', { method: 'POST', body: JSON.stringify(body) }) } export function updateCategory(id: number, body: Partial): Promise { return request(`/categories/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function deleteCategory(id: number): Promise<{ ok: boolean }> { return request(`/categories/${id}`, { method: 'DELETE' }) } // Tasks export interface TaskFilters { status?: string category_id?: number location_id?: number asset_id?: number priority?: string assigned_to?: string contractor_id?: number q?: string unoccupied?: boolean } export function fetchTasks(filters: TaskFilters = {}): Promise { const params = new URLSearchParams() for (const [k, v] of Object.entries(filters)) { if (v !== undefined && v !== null && v !== '' && v !== false) params.set(k, String(v)) } const qs = params.toString() return request(`/tasks${qs ? `?${qs}` : ''}`) } export function fetchTask(id: number): Promise { return request(`/tasks/${id}`) } export function createTask(body: Record): Promise { return request('/tasks', { method: 'POST', body: JSON.stringify(body) }) } export function updateTask(id: number, body: Record): Promise { return request(`/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function deleteTask(id: number): Promise<{ ok: boolean }> { return request(`/tasks/${id}`, { method: 'DELETE' }) } export function resolveTask(id: number, body: { status: 'temporary_fix' | 'fixed' completed_by?: string completed_by_name?: string cost?: string cost_notes?: string note?: string }): Promise { return request(`/tasks/${id}/resolve`, { method: 'POST', body: JSON.stringify(body) }) } export function addComment(id: number, note: string, addToTemplate = false): Promise<{ ok: boolean; added_to_template: boolean }> { return request(`/tasks/${id}/comments`, { method: 'POST', body: JSON.stringify({ note, add_to_template: addToTemplate }) }) } export function fetchOccupancy(): Promise<{ date: string; occupied_site_ids: string[]; arriving_site_ids: string[] }> { return request('/occupancy') } // Photos — multipart, so no JSON content-type header export async function uploadTaskPhoto(taskId: number, file: File, stage: string): Promise { const form = new FormData() form.append('stage', stage) form.append('file', file) const res = await fetch(`${BASE}/tasks/${taskId}/photos`, { method: 'POST', credentials: 'include', body: form }) if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Upload failed: ${res.status}`) } return res.json() } export function deletePhoto(id: number): Promise<{ ok: boolean }> { return request(`/photos/${id}`, { method: 'DELETE' }) } export function photoUrl(filePath: string): string { return `${BASE}/uploads${filePath}` } // History export interface HistoryFilters { q?: string from?: string to?: string category_id?: number location_id?: number asset_id?: number include_temporary?: boolean limit?: number offset?: number } export function fetchHistory(filters: HistoryFilters = {}): Promise<{ tasks: Task[] totals: { count: number; total_cost: string | null; avg_days_to_fix: string | null } }> { const params = new URLSearchParams() for (const [k, v] of Object.entries(filters)) { if (v !== undefined && v !== null && v !== '' && v !== false) params.set(k, String(v)) } const qs = params.toString() return request(`/history${qs ? `?${qs}` : ''}`) } export function historyExportUrl(filters: HistoryFilters = {}): string { const params = new URLSearchParams() for (const [k, v] of Object.entries(filters)) { if (v !== undefined && v !== null && v !== '' && v !== false) params.set(k, String(v)) } const qs = params.toString() return `${BASE}/history/export${qs ? `?${qs}` : ''}` } // Assets export function fetchAssets(includeInactive = false): Promise { return request(`/assets${includeInactive ? '?include_inactive=true' : ''}`) } export function fetchAsset(id: number): Promise { return request(`/assets/${id}`) } export function createAsset(body: Record): Promise { return request('/assets', { method: 'POST', body: JSON.stringify(body) }) } export function updateAsset(id: number, body: Record): Promise { return request(`/assets/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } // Contractors export function fetchContractors(includeInactive = false): Promise { return request(`/contractors${includeInactive ? '?include_inactive=true' : ''}`) } export function fetchContractor(id: number): Promise { return request(`/contractors/${id}`) } export function createContractor(body: Record): Promise { return request('/contractors', { method: 'POST', body: JSON.stringify(body) }) } export function updateContractor(id: number, body: Record): Promise { return request(`/contractors/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export async function uploadContractorDoc(contractorId: number, file: File, docType: string, expiryDate: string): Promise { const form = new FormData() form.append('doc_type', docType) form.append('expiry_date', expiryDate) form.append('file', file) const res = await fetch(`${BASE}/contractors/${contractorId}/docs`, { method: 'POST', credentials: 'include', body: form }) if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Upload failed: ${res.status}`) } return res.json() } export function deleteContractorDoc(id: number): Promise<{ ok: boolean }> { return request(`/contractor-docs/${id}`, { method: 'DELETE' }) } // Templates (recurring tasks) export function fetchTemplates(): Promise { return request('/templates') } export function createTemplate(body: Record): Promise