mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
1st approach for projects, items, db structure
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
import { apiFetch } from './http-client';
|
||||
|
||||
export type HealthResponse = {
|
||||
status: string;
|
||||
service: string;
|
||||
};
|
||||
|
||||
export function getHealth() {
|
||||
return apiFetch<HealthResponse>('/health');
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api';
|
||||
|
||||
type ApiFetchOptions = Omit<RequestInit, 'body'> & {
|
||||
body?: Record<string, unknown>;
|
||||
body?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export async function apiFetch<TResponse>(path: string, options: ApiFetchOptions = {}): Promise<TResponse> {
|
||||
@@ -18,7 +18,28 @@ export async function apiFetch<TResponse>(path: string, options: ApiFetchOptions
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, init);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status}`);
|
||||
let message = `Request failed: ${response.status}`;
|
||||
|
||||
try {
|
||||
const errorPayload = await response.clone().json();
|
||||
const extractedMessage =
|
||||
(errorPayload && errorPayload.error && errorPayload.error.message) ??
|
||||
errorPayload.message;
|
||||
if (typeof extractedMessage === 'string' && extractedMessage.trim().length > 0) {
|
||||
message = extractedMessage;
|
||||
}
|
||||
} catch {
|
||||
// ignore JSON parsing issues - fall back to status text when available
|
||||
if (response.statusText) {
|
||||
message = `${message} ${response.statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null as TResponse;
|
||||
}
|
||||
|
||||
return response.json() as Promise<TResponse>;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { apiFetch } from './http-client';
|
||||
|
||||
export type PriceCondition = 'new' | 'used';
|
||||
export type PriceSourceType = 'url' | 'manual';
|
||||
|
||||
export interface ItemPrice {
|
||||
id: string;
|
||||
itemId: string;
|
||||
condition: PriceCondition;
|
||||
amount: number;
|
||||
currency: string;
|
||||
sourceType: PriceSourceType;
|
||||
sourceUrlId: string | null;
|
||||
sourceUrl: string | null;
|
||||
sourceNote: string | null;
|
||||
note: string | null;
|
||||
observedAt: string;
|
||||
isPrimary: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ListResponseMeta {
|
||||
limit: number;
|
||||
offset: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ItemPricesResponse {
|
||||
data: ItemPrice[];
|
||||
meta: ListResponseMeta;
|
||||
}
|
||||
|
||||
export interface FetchItemPricesParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
condition?: PriceCondition;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const fetchItemPrices = async (
|
||||
itemId: string,
|
||||
params: FetchItemPricesParams = {}
|
||||
): Promise<ItemPricesResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
searchParams.set('limit', String(params.limit));
|
||||
}
|
||||
|
||||
if (params.offset !== undefined) {
|
||||
searchParams.set('offset', String(params.offset));
|
||||
}
|
||||
|
||||
if (params.condition) {
|
||||
searchParams.set('condition', params.condition);
|
||||
}
|
||||
|
||||
const query = searchParams.toString();
|
||||
const path = query
|
||||
? `/items/${itemId}/prices?${query}`
|
||||
: `/items/${itemId}/prices`;
|
||||
|
||||
return apiFetch<ItemPricesResponse>(path, { signal: params.signal });
|
||||
};
|
||||
|
||||
export interface CreateItemPricePayload {
|
||||
condition: PriceCondition;
|
||||
amount: number;
|
||||
currency: string;
|
||||
sourceType: PriceSourceType;
|
||||
sourceUrl?: string | null;
|
||||
sourceUrlId?: string | null;
|
||||
sourceNote?: string | null;
|
||||
note?: string | null;
|
||||
observedAt?: string;
|
||||
isPrimary?: boolean;
|
||||
}
|
||||
|
||||
interface SingleItemPriceResponse {
|
||||
data: ItemPrice;
|
||||
}
|
||||
|
||||
export const createItemPrice = async (
|
||||
itemId: string,
|
||||
payload: CreateItemPricePayload
|
||||
): Promise<ItemPrice> => {
|
||||
const response = await apiFetch<SingleItemPriceResponse>(
|
||||
`/items/${itemId}/prices`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
condition: payload.condition,
|
||||
amount: payload.amount,
|
||||
currency: payload.currency,
|
||||
sourceType: payload.sourceType,
|
||||
sourceUrl: payload.sourceUrl ?? null,
|
||||
sourceUrlId: payload.sourceUrlId ?? null,
|
||||
sourceNote: payload.sourceNote ?? null,
|
||||
note: payload.note ?? null,
|
||||
observedAt: payload.observedAt ?? null,
|
||||
isPrimary: payload.isPrimary ?? false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { apiFetch } from './http-client';
|
||||
|
||||
export type ItemStatus = 'active' | 'rejected';
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
projectId: string;
|
||||
manufacturer: string | null;
|
||||
model: string;
|
||||
sourceUrlId: string | null;
|
||||
sourceUrl: string | null;
|
||||
status: ItemStatus;
|
||||
note: string | null;
|
||||
attributes: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
priceSummary: ItemPriceSummary | null;
|
||||
}
|
||||
|
||||
export interface ItemPriceSummary {
|
||||
minAmount: number;
|
||||
maxAmount: number;
|
||||
currency: string | null;
|
||||
priceCount: number;
|
||||
hasMixedCurrency: boolean;
|
||||
}
|
||||
|
||||
interface ListResponseMeta {
|
||||
limit: number;
|
||||
offset: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ProjectItemsResponse {
|
||||
data: Item[];
|
||||
meta: ListResponseMeta;
|
||||
}
|
||||
|
||||
export interface FetchProjectItemsParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ItemStatus;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const fetchProjectItems = async (
|
||||
projectId: string,
|
||||
params: FetchProjectItemsParams = {}
|
||||
): Promise<ProjectItemsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
searchParams.set('limit', String(params.limit));
|
||||
}
|
||||
|
||||
if (params.offset !== undefined) {
|
||||
searchParams.set('offset', String(params.offset));
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
|
||||
const query = searchParams.toString();
|
||||
const path = query
|
||||
? `/projects/${projectId}/items?${query}`
|
||||
: `/projects/${projectId}/items`;
|
||||
|
||||
return apiFetch<ProjectItemsResponse>(path, { signal: params.signal });
|
||||
};
|
||||
|
||||
export interface CreateItemPayload {
|
||||
manufacturer?: string | null;
|
||||
model: string;
|
||||
status?: ItemStatus;
|
||||
note?: string | null;
|
||||
attributes?: Record<string, unknown>;
|
||||
sourceUrl?: string | null;
|
||||
sourceUrlId?: string | null;
|
||||
}
|
||||
|
||||
interface SingleItemResponse {
|
||||
data: Item;
|
||||
}
|
||||
|
||||
export const createItem = async (
|
||||
projectId: string,
|
||||
payload: CreateItemPayload
|
||||
): Promise<Item> => {
|
||||
const response = await apiFetch<SingleItemResponse>(
|
||||
`/projects/${projectId}/items`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
manufacturer: payload.manufacturer ?? null,
|
||||
model: payload.model,
|
||||
status: payload.status ?? 'active',
|
||||
note: payload.note ?? null,
|
||||
attributes: payload.attributes ?? {},
|
||||
sourceUrl: payload.sourceUrl ?? null,
|
||||
sourceUrlId: payload.sourceUrlId ?? null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { apiFetch } from './http-client';
|
||||
|
||||
export type ProjectStatus = 'active' | 'archived';
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: ProjectStatus;
|
||||
attributes: string[];
|
||||
priorityRules: unknown;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ListResponseMeta {
|
||||
limit: number;
|
||||
offset: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ProjectsListResponse {
|
||||
data: Project[];
|
||||
meta: ListResponseMeta;
|
||||
}
|
||||
|
||||
export interface FetchProjectsParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ProjectStatus;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const fetchProjects = async (
|
||||
params: FetchProjectsParams = {}
|
||||
): Promise<ProjectsListResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
searchParams.set('limit', String(params.limit));
|
||||
}
|
||||
|
||||
if (params.offset !== undefined) {
|
||||
searchParams.set('offset', String(params.offset));
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
|
||||
const query = searchParams.toString();
|
||||
const path = query ? `/projects?${query}` : '/projects';
|
||||
|
||||
return apiFetch<ProjectsListResponse>(path, { signal: params.signal });
|
||||
};
|
||||
|
||||
export interface CreateProjectPayload {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
status?: ProjectStatus;
|
||||
attributes?: string[];
|
||||
priorityRules?: unknown;
|
||||
}
|
||||
|
||||
interface SingleProjectResponse {
|
||||
data: Project;
|
||||
}
|
||||
|
||||
export interface UpdateProjectPayload {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
status?: ProjectStatus;
|
||||
attributes?: string[];
|
||||
priorityRules?: unknown;
|
||||
}
|
||||
|
||||
export const createProject = async (
|
||||
payload: CreateProjectPayload
|
||||
): Promise<Project> => {
|
||||
const response = await apiFetch<SingleProjectResponse>('/projects', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: payload.name,
|
||||
description: payload.description ?? null,
|
||||
status: payload.status ?? undefined,
|
||||
attributes: payload.attributes ?? [],
|
||||
priorityRules: payload.priorityRules ?? []
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export interface FetchProjectParams {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const fetchProject = async (
|
||||
projectId: string,
|
||||
params: FetchProjectParams = {}
|
||||
): Promise<Project> => {
|
||||
const response = await apiFetch<SingleProjectResponse>(
|
||||
`/projects/${projectId}`,
|
||||
{ signal: params.signal }
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateProject = async (
|
||||
projectId: string,
|
||||
payload: UpdateProjectPayload
|
||||
): Promise<Project> => {
|
||||
const response = await apiFetch<SingleProjectResponse>(`/projects/${projectId}`, {
|
||||
method: 'PATCH',
|
||||
body: payload
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
Reference in New Issue
Block a user