Implement medicine library and cabinet

This commit is contained in:
Aerilyn Weber 2026-03-28 08:19:48 +09:00
parent db79af06f7
commit 1f66fab30f
72 changed files with 7642 additions and 319 deletions

View file

@ -7,6 +7,10 @@ class ApiClient {
this._accessToken = token;
}
public get hasToken(): boolean {
return this._accessToken !== null;
}
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
@ -19,15 +23,26 @@ class ApiClient {
return headers;
}
private async handleResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
let message: string;
try {
const body = await res.json();
message = body.message || `Request failed: ${res.status}`;
} catch {
message = `Request failed: ${res.status} ${res.statusText}`;
}
throw new Error(message);
}
if (res.status === 204) return undefined as T;
return res.json();
}
public async get<T>(url: string): Promise<T> {
const res = await fetch(`${BASE_URL}${url}`, {
headers: this.getHeaders(),
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(error.message || `Request failed: ${res.status}`);
}
return res.json();
return this.handleResponse<T>(res);
}
public async post<T>(url: string, body?: unknown): Promise<T> {
@ -36,11 +51,7 @@ class ApiClient {
headers: this.getHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(error.message || `Request failed: ${res.status}`);
}
return res.json();
return this.handleResponse<T>(res);
}
public async patch<T>(url: string, body: unknown): Promise<T> {
@ -49,24 +60,19 @@ class ApiClient {
headers: this.getHeaders(),
body: JSON.stringify(body),
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(error.message || `Request failed: ${res.status}`);
}
return res.json();
return this.handleResponse<T>(res);
}
public async delete<T = void>(url: string): Promise<T> {
const headers: Record<string, string> = {};
if (this._accessToken) {
headers['Authorization'] = `Bearer ${this._accessToken}`;
}
const res = await fetch(`${BASE_URL}${url}`, {
method: 'DELETE',
headers: this.getHeaders(),
headers,
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(error.message || `Request failed: ${res.status}`);
}
if (res.status === 204) return undefined as T;
return res.json();
return this.handleResponse<T>(res);
}
}

View file

@ -0,0 +1,86 @@
import { apiClient } from './api-client';
import type { z } from 'zod/v4';
import type {
CabinetItemResponseSchema,
CabinetItemListResponseSchema,
CabinetSummaryResponseSchema,
CreateCabinetItemInput,
UpdateCabinetItemInput,
AdjustQuantityInput,
} from '@meshitrack/shared';
type CabinetItemResponse = z.infer<typeof CabinetItemResponseSchema>;
type CabinetItemListResponse = z.infer<typeof CabinetItemListResponseSchema>;
type CabinetSummaryResponse = z.infer<typeof CabinetSummaryResponseSchema>;
export async function listCabinetItems(
householdId: string,
query?: {
medicineId?: string;
status?: string;
expiringWithin?: number;
cursor?: string;
limit?: number;
},
): Promise<CabinetItemListResponse> {
const params = new URLSearchParams();
if (query?.medicineId) params.set('medicineId', query.medicineId);
if (query?.status) params.set('status', query.status);
if (query?.expiringWithin) params.set('expiringWithin', String(query.expiringWithin));
if (query?.cursor) params.set('cursor', query.cursor);
if (query?.limit) params.set('limit', String(query.limit));
const qs = params.toString();
return apiClient.get<CabinetItemListResponse>(
`/households/${householdId}/cabinet${qs ? `?${qs}` : ''}`,
);
}
export async function getCabinetItem(
householdId: string,
id: string,
): Promise<CabinetItemResponse> {
return apiClient.get<CabinetItemResponse>(`/households/${householdId}/cabinet/${id}`);
}
export async function getCabinetSummary(householdId: string): Promise<CabinetSummaryResponse> {
return apiClient.get<CabinetSummaryResponse>(`/households/${householdId}/cabinet/summary`);
}
export async function getExpiringSoon(
householdId: string,
days = 30,
): Promise<{ data: CabinetItemResponse[] }> {
return apiClient.get<{ data: CabinetItemResponse[] }>(
`/households/${householdId}/cabinet/expiring-soon?days=${days}`,
);
}
export async function createCabinetItem(
householdId: string,
data: CreateCabinetItemInput,
): Promise<CabinetItemResponse> {
return apiClient.post<CabinetItemResponse>(`/households/${householdId}/cabinet`, data);
}
export async function updateCabinetItem(
householdId: string,
id: string,
data: UpdateCabinetItemInput,
): Promise<CabinetItemResponse> {
return apiClient.patch<CabinetItemResponse>(`/households/${householdId}/cabinet/${id}`, data);
}
export async function adjustCabinetItemQuantity(
householdId: string,
id: string,
data: AdjustQuantityInput,
): Promise<CabinetItemResponse> {
return apiClient.post<CabinetItemResponse>(
`/households/${householdId}/cabinet/${id}/adjust`,
data,
);
}
export async function deleteCabinetItem(householdId: string, id: string): Promise<void> {
return apiClient.delete(`/households/${householdId}/cabinet/${id}`);
}

View file

@ -0,0 +1,28 @@
import { apiClient } from './api-client';
import type { z } from 'zod/v4';
import type { HouseholdResponseSchema, UpdateHouseholdInput } from '@meshitrack/shared';
type HouseholdResponse = z.infer<typeof HouseholdResponseSchema>;
export async function createHousehold(name: string): Promise<HouseholdResponse> {
return apiClient.post<HouseholdResponse>('/households', { name });
}
export async function getHousehold(id: string): Promise<HouseholdResponse> {
return apiClient.get<HouseholdResponse>(`/households/${id}`);
}
export async function updateHousehold(
id: string,
data: UpdateHouseholdInput,
): Promise<HouseholdResponse> {
return apiClient.patch<HouseholdResponse>(`/households/${id}`, data);
}
export async function generateInviteCode(id: string): Promise<HouseholdResponse> {
return apiClient.post<HouseholdResponse>(`/households/${id}/invite`);
}
export async function joinHousehold(inviteCode: string): Promise<HouseholdResponse> {
return apiClient.post<HouseholdResponse>('/households/join', { inviteCode });
}

View file

@ -0,0 +1,96 @@
import { apiClient } from './api-client';
import type { z } from 'zod/v4';
import type {
MedicineResponseSchema,
MedicineListResponseSchema,
MedicineProductResponseSchema,
MedicineProductListResponseSchema,
CreateMedicineInput,
UpdateMedicineInput,
CreateMedicineProductInput,
UpdateMedicineProductInput,
} from '@meshitrack/shared';
type MedicineResponse = z.infer<typeof MedicineResponseSchema>;
type MedicineListResponse = z.infer<typeof MedicineListResponseSchema>;
type MedicineProductResponse = z.infer<typeof MedicineProductResponseSchema>;
type MedicineProductListResponse = z.infer<typeof MedicineProductListResponseSchema>;
export async function listMedicines(
householdId: string,
query?: { q?: string; category?: string; form?: string; cursor?: string; limit?: number },
): Promise<MedicineListResponse> {
const params = new URLSearchParams();
if (query?.q) params.set('q', query.q);
if (query?.category) params.set('category', query.category);
if (query?.form) params.set('form', query.form);
if (query?.cursor) params.set('cursor', query.cursor);
if (query?.limit) params.set('limit', String(query.limit));
const qs = params.toString();
return apiClient.get<MedicineListResponse>(
`/households/${householdId}/medicines${qs ? `?${qs}` : ''}`,
);
}
export async function getMedicine(householdId: string, id: string): Promise<MedicineResponse> {
return apiClient.get<MedicineResponse>(`/households/${householdId}/medicines/${id}`);
}
export async function createMedicine(
householdId: string,
data: CreateMedicineInput,
): Promise<MedicineResponse> {
return apiClient.post<MedicineResponse>(`/households/${householdId}/medicines`, data);
}
export async function updateMedicine(
householdId: string,
id: string,
data: UpdateMedicineInput,
): Promise<MedicineResponse> {
return apiClient.patch<MedicineResponse>(`/households/${householdId}/medicines/${id}`, data);
}
export async function deleteMedicine(householdId: string, id: string): Promise<void> {
return apiClient.delete(`/households/${householdId}/medicines/${id}`);
}
export async function listMedicineProducts(
householdId: string,
medicineId: string,
query?: { cursor?: string; limit?: number },
): Promise<MedicineProductListResponse> {
const params = new URLSearchParams();
if (query?.cursor) params.set('cursor', query.cursor);
if (query?.limit) params.set('limit', String(query.limit));
const qs = params.toString();
return apiClient.get<MedicineProductListResponse>(
`/households/${householdId}/medicines/${medicineId}/products${qs ? `?${qs}` : ''}`,
);
}
export async function createMedicineProduct(
householdId: string,
medicineId: string,
data: CreateMedicineProductInput,
): Promise<MedicineProductResponse> {
return apiClient.post<MedicineProductResponse>(
`/households/${householdId}/medicines/${medicineId}/products`,
data,
);
}
export async function updateMedicineProduct(
householdId: string,
id: string,
data: UpdateMedicineProductInput,
): Promise<MedicineProductResponse> {
return apiClient.patch<MedicineProductResponse>(
`/households/${householdId}/medicine-products/${id}`,
data,
);
}
export async function deleteMedicineProduct(householdId: string, id: string): Promise<void> {
return apiClient.delete(`/households/${householdId}/medicine-products/${id}`);
}