1st approach for projects, items, db structure

This commit is contained in:
Jurgis Sakalauskas
2025-10-13 14:04:22 +03:00
parent ad2cf25bfd
commit afc3da3e65
45 changed files with 4743 additions and 117 deletions
-10
View File
@@ -1,10 +0,0 @@
import { apiFetch } from './http-client';
export type HealthResponse = {
status: string;
service: string;
};
export function getHealth() {
return apiFetch<HealthResponse>('/health');
}
+23 -2
View File
@@ -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>;
+108
View File
@@ -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;
};
+107
View File
@@ -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;
};
+120
View File
@@ -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;
};