Implement medicine library and cabinet
This commit is contained in:
parent
db79af06f7
commit
1f66fab30f
72 changed files with 7642 additions and 319 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
86
packages/web/src/services/cabinet.ts
Normal file
86
packages/web/src/services/cabinet.ts
Normal 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}`);
|
||||
}
|
||||
28
packages/web/src/services/households.ts
Normal file
28
packages/web/src/services/households.ts
Normal 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 });
|
||||
}
|
||||
96
packages/web/src/services/medicines.ts
Normal file
96
packages/web/src/services/medicines.ts
Normal 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}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue