Backend: receipt upload -> LLM vision extraction (OpenAI-compatible, provider-agnostic), item review/edit, per-group splitting against Cospend projects/members, highlight+upload via WebDAV, public share link, bill creation via Cospend's OCS API (verified against real source, not just doc summaries). Frontend: capture -> review -> group -> summary flow as an installable PWA. install.sh / run.sh (venv + npm, tmux session) instead of Docker, per the ~/Projects/gain pattern.
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import type { CospendProject, Group, Item, Member, Receipt, SubmitResult } from './types'
|
|
|
|
const BASE = import.meta.env.VITE_API_BASE_URL ?? ''
|
|
|
|
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(`${BASE}/api${path}`, {
|
|
...init,
|
|
headers: init?.body instanceof FormData ? init.headers : { 'Content-Type': 'application/json', ...init?.headers },
|
|
})
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}))
|
|
throw new Error(body.error || `${res.status} ${res.statusText}`)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
export const api = {
|
|
uploadReceipt(
|
|
file: File | Blob,
|
|
): Promise<{ id: string; items: Item[]; store_name: string | null; date: string }> {
|
|
const form = new FormData()
|
|
form.append('image', file, 'receipt.jpg')
|
|
return req('/receipts', { method: 'POST', body: form })
|
|
},
|
|
|
|
getReceipt(id: string): Promise<Receipt> {
|
|
return req(`/receipts/${id}`)
|
|
},
|
|
|
|
updateItems(id: string, items: Item[]): Promise<Receipt> {
|
|
return req(`/receipts/${id}/items`, { method: 'PATCH', body: JSON.stringify({ items }) })
|
|
},
|
|
|
|
updateMeta(id: string, meta: { store_name?: string; date?: string }): Promise<Receipt> {
|
|
return req(`/receipts/${id}/meta`, { method: 'PATCH', body: JSON.stringify(meta) })
|
|
},
|
|
|
|
getProjects(): Promise<{ projects: CospendProject[]; default_project_id: string | null }> {
|
|
return req('/cospend/projects')
|
|
},
|
|
|
|
getMembers(projectId: string): Promise<Member[]> {
|
|
return req(`/cospend/projects/${projectId}/members`)
|
|
},
|
|
|
|
createGroup(
|
|
receiptId: string,
|
|
group: {
|
|
name: string
|
|
cospend_project_id: string
|
|
payer_member_id: string
|
|
member_ids: string[]
|
|
item_ids: string[]
|
|
},
|
|
): Promise<{ id: string }> {
|
|
return req(`/receipts/${receiptId}/groups`, { method: 'POST', body: JSON.stringify(group) })
|
|
},
|
|
|
|
submitGroup(receiptId: string, groupId: string): Promise<SubmitResult> {
|
|
return req(`/receipts/${receiptId}/groups/${groupId}/submit`, { method: 'POST' })
|
|
},
|
|
|
|
listGroups(receiptId: string): Promise<Group[]> {
|
|
return req(`/receipts/${receiptId}/groups`)
|
|
},
|
|
}
|