Standalone expenses (date, description, category, amount) with the same equal/own/payer split machinery as entry costs, merged into the costs panel and settle-up as a single expense bucket. Self-fetching card with per-day grouping, client-side sorting, and quick-add. CSV export interleaves expenses with priced entries, one share column per member (UTF-8 BOM, RFC 4180, formula-injection guard on text cells). Also fixes trip deletion, which hit a foreign-key violation and rolled back for any trip with checklist items.
106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
// Thin fetch wrapper for the Trip Plan REST API (see docs/API.md).
|
|
// Same-origin, cookies included. Server errors ({error}) become thrown Errors
|
|
// whose message is the server's human-readable text and `.status` the code.
|
|
|
|
async function request(method, path, body) {
|
|
const opts = {
|
|
method,
|
|
credentials: 'same-origin',
|
|
headers: {},
|
|
};
|
|
if (body !== undefined) {
|
|
opts.headers['Content-Type'] = 'application/json';
|
|
opts.body = JSON.stringify(body);
|
|
}
|
|
|
|
let res;
|
|
try {
|
|
res = await fetch(path, opts);
|
|
} catch (networkErr) {
|
|
const err = new Error('Network error — is the server running?');
|
|
err.cause = networkErr;
|
|
err.status = 0;
|
|
throw err;
|
|
}
|
|
|
|
if (res.status === 204) return null;
|
|
|
|
const text = await res.text();
|
|
let data = null;
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
data = null;
|
|
}
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const message = (data && data.error) || `Request failed (${res.status})`;
|
|
const err = new Error(message);
|
|
err.status = res.status;
|
|
err.body = data;
|
|
throw err;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
const get = (p) => request('GET', p);
|
|
const post = (p, b) => request('POST', p, b);
|
|
const patch = (p, b) => request('PATCH', p, b);
|
|
const del = (p) => request('DELETE', p);
|
|
|
|
export const api = {
|
|
auth: {
|
|
// Mullvad-style: create an account (server returns the one-time token).
|
|
createAccount: () => post('/api/auth/account', {}),
|
|
login: (token) => post('/api/auth/login', { token }),
|
|
logout: () => post('/api/auth/logout'),
|
|
me: () => get('/api/auth/me'),
|
|
updateMe: (patchBody) => patch('/api/auth/me', patchBody),
|
|
},
|
|
trips: {
|
|
list: () => get('/api/trips'),
|
|
create: (payload) => post('/api/trips', payload),
|
|
get: (id) => get(`/api/trips/${id}`),
|
|
update: (id, patchBody) => patch(`/api/trips/${id}`, patchBody),
|
|
reorder: (id, sort_order) => patch(`/api/trips/${id}/order`, { sort_order }),
|
|
remove: (id) => del(`/api/trips/${id}`),
|
|
join: (code) => post('/api/trips/join', { code }),
|
|
regenerateJoinCode: (id) => post(`/api/trips/${id}/join-code`, {}),
|
|
removeMember: (id, userId) => del(`/api/trips/${id}/members/${userId}`),
|
|
route: (id) => get(`/api/trips/${id}/route`),
|
|
costs: (id) => get(`/api/trips/${id}/costs`),
|
|
regenerateTransports: (id) => post(`/api/trips/${id}/transports/regenerate`, {}),
|
|
},
|
|
directions: (from, to, via = []) => get(
|
|
`/api/directions?from=${from.lat},${from.lng}&to=${to.lat},${to.lng}` +
|
|
(via.length ? `&via=${via.map((w) => `${w.lat},${w.lng}`).join('|')}` : ''),
|
|
),
|
|
entries: {
|
|
create: (tripId, payload) => post(`/api/trips/${tripId}/entries`, payload),
|
|
update: (id, patchBody) => patch(`/api/entries/${id}`, patchBody),
|
|
remove: (id) => del(`/api/entries/${id}`),
|
|
},
|
|
geocode: (q) => get(`/api/geocode?q=${encodeURIComponent(q)}`),
|
|
airports: (q) => get(`/api/airports?q=${encodeURIComponent(q)}`),
|
|
checklist: {
|
|
list: (tripId) => get(`/api/trips/${tripId}/checklist`),
|
|
create: (tripId, payload) => post(`/api/trips/${tripId}/checklist`, payload),
|
|
update: (itemId, patchBody) => patch(`/api/checklist/${itemId}`, patchBody),
|
|
remove: (itemId) => del(`/api/checklist/${itemId}`),
|
|
reset: (tripId) => post(`/api/trips/${tripId}/checklist/reset`, {}),
|
|
suggestions: (tripId) => get(`/api/trips/${tripId}/checklist/suggestions`),
|
|
addSuggestions: (tripId, keys, personal) =>
|
|
post(`/api/trips/${tripId}/checklist/suggestions`, personal === undefined ? { keys } : { keys, personal }),
|
|
},
|
|
expenses: {
|
|
list: (tripId) => get(`/api/trips/${tripId}/expenses`),
|
|
create: (tripId, payload) => post(`/api/trips/${tripId}/expenses`, payload),
|
|
update: (expenseId, patchBody) => patch(`/api/expenses/${expenseId}`, patchBody),
|
|
remove: (expenseId) => del(`/api/expenses/${expenseId}`),
|
|
},
|
|
};
|
|
|
|
export default api;
|