WHY: migrating from Supabase BaaS to self-hosted iloom backend (Go microservices) deployed on K3s. Supabase client and realtime subscriptions no longer applicable. HOW: - add src/api/ layer (client.ts, auth.ts, purchaser.ts, textile.ts, washing.ts, websocket.ts) targeting iloom-gateway REST endpoints - rewrite all hooks (usePlanData, useInventoryData, useDashboardData, useRealtime, etc.) to use new API client instead of Supabase queries - rewrite all page/component data fetching and mutations accordingly - delete src/supabase/client.ts, remove @supabase/supabase-js dep - add Dockerfile (multi-stage node build + nginx) and nginx.conf with reverse proxy to iloom-gateway for /api/ and /ws/ routes - adjust webpack.config.js for API_URL environment variable injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.1 KiB
TypeScript
148 lines
4.1 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { purchaserApi } from '../api';
|
|
import type { PlanDetail } from '../api/purchaser';
|
|
import { CACHE_CONFIG, PAGINATION_CONFIG } from '../config/app';
|
|
import { usePlanRealtime } from './useRealtime';
|
|
import type {
|
|
PlanWithSteps,
|
|
Company,
|
|
PlanFactory,
|
|
YarnRatio,
|
|
} from '../types';
|
|
|
|
interface UsePlanDataOptions {
|
|
companyId: string | null;
|
|
pageSize?: number;
|
|
}
|
|
|
|
interface PlanDataState {
|
|
plans: PlanWithSteps[];
|
|
factories: Company[];
|
|
planFactories: PlanFactory[];
|
|
operatorNames: Record<string, string>;
|
|
yarnRatios: Record<string, YarnRatio[]>;
|
|
loading: boolean;
|
|
loadingMore: boolean;
|
|
hasMore: boolean;
|
|
currentPage: number;
|
|
error: Error | null;
|
|
}
|
|
|
|
function mapPlanDetail(plan: PlanDetail): PlanWithSteps {
|
|
return {
|
|
...plan,
|
|
processSteps: plan.process_steps,
|
|
inventoryRecords: plan.inventory_records,
|
|
priceHistory: plan.price_history,
|
|
};
|
|
}
|
|
|
|
export function usePlanData({ companyId, pageSize = PAGINATION_CONFIG.defaultPageSize }: UsePlanDataOptions) {
|
|
const [state, setState] = useState<PlanDataState>({
|
|
plans: [],
|
|
factories: [],
|
|
planFactories: [],
|
|
operatorNames: {},
|
|
yarnRatios: {},
|
|
loading: true,
|
|
loadingMore: false,
|
|
hasMore: true,
|
|
currentPage: 1,
|
|
error: null
|
|
});
|
|
|
|
const cacheRef = useRef<{
|
|
data: PlanDataState | null;
|
|
timestamp: number;
|
|
}>({ data: null, timestamp: 0 });
|
|
|
|
const CACHE_DURATION = CACHE_CONFIG.planDataDuration;
|
|
|
|
const fetchData = useCallback(async (page: number = 1, append: boolean = false) => {
|
|
if (!companyId) {
|
|
setState(prev => ({ ...prev, loading: false }));
|
|
return;
|
|
}
|
|
|
|
const isFirstPage = page === 1;
|
|
|
|
if (isFirstPage) {
|
|
const now = Date.now();
|
|
if (cacheRef.current.data && now - cacheRef.current.timestamp < CACHE_DURATION) {
|
|
setState(cacheRef.current.data);
|
|
return;
|
|
}
|
|
setState(prev => ({ ...prev, loading: true, error: null }));
|
|
} else {
|
|
setState(prev => ({ ...prev, loadingMore: true }));
|
|
}
|
|
|
|
try {
|
|
const res = await purchaserApi.listPlans({ page, page_size: pageSize });
|
|
const { plans: planDetails, factories, plan_factories, operator_names } = res.data;
|
|
|
|
const plansWithSteps = planDetails.map(mapPlanDetail);
|
|
|
|
const yarnRatiosMap: Record<string, YarnRatio[]> = {};
|
|
planDetails.forEach(p => {
|
|
if (p.yarn_ratios && p.yarn_ratios.length > 0) {
|
|
yarnRatiosMap[p.id] = p.yarn_ratios;
|
|
}
|
|
});
|
|
|
|
setState(prev => {
|
|
const newState: PlanDataState = {
|
|
plans: append ? [...prev.plans, ...plansWithSteps] : plansWithSteps,
|
|
factories: isFirstPage ? factories : prev.factories,
|
|
planFactories: isFirstPage ? plan_factories : prev.planFactories,
|
|
operatorNames: isFirstPage ? operator_names : { ...prev.operatorNames, ...operator_names },
|
|
yarnRatios: isFirstPage ? yarnRatiosMap : { ...prev.yarnRatios, ...yarnRatiosMap },
|
|
loading: false,
|
|
loadingMore: false,
|
|
hasMore: res.meta?.has_more ?? planDetails.length === pageSize,
|
|
currentPage: page,
|
|
error: null
|
|
};
|
|
|
|
if (isFirstPage) {
|
|
cacheRef.current = { data: newState, timestamp: Date.now() };
|
|
}
|
|
|
|
return newState;
|
|
});
|
|
} catch (err) {
|
|
setState(prev => ({
|
|
...prev,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: err instanceof Error ? err : new Error('Unknown error')
|
|
}));
|
|
}
|
|
}, [companyId, pageSize]);
|
|
|
|
const loadMore = useCallback(() => {
|
|
if (state.loadingMore || !state.hasMore) return;
|
|
fetchData(state.currentPage + 1, true);
|
|
}, [fetchData, state.currentPage, state.hasMore, state.loadingMore]);
|
|
|
|
const refetch = useCallback(() => {
|
|
cacheRef.current.timestamp = 0;
|
|
fetchData(1, false);
|
|
}, [fetchData]);
|
|
|
|
useEffect(() => {
|
|
fetchData(1, false);
|
|
}, [fetchData]);
|
|
|
|
usePlanRealtime(companyId, () => {
|
|
cacheRef.current.timestamp = 0;
|
|
fetchData(1, false);
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
loadMore,
|
|
refetch
|
|
};
|
|
}
|