Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -0,0 +1,246 @@
/**
* Dashboard React Query hooks
* Aggregates data from multiple services for dashboard metrics
*/
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useSalesAnalytics } from './sales';
import { useOrdersDashboard } from './orders';
import { inventoryService } from '../services/inventory';
import { getAlertAnalytics } from '../services/alert_analytics';
import { ApiError } from '../client/apiClient';
import type { InventoryDashboardSummary } from '../types/dashboard';
import type { AlertAnalytics } from '../services/alert_analytics';
import type { SalesAnalytics } from '../types/sales';
import type { OrdersDashboardSummary } from '../types/orders';
export interface DashboardStats {
// Alert metrics
activeAlerts: number;
criticalAlerts: number;
// Order metrics
pendingOrders: number;
ordersToday: number;
ordersTrend: number; // percentage change
// Sales metrics
salesToday: number;
salesTrend: number; // percentage change
salesCurrency: string;
// Inventory metrics
criticalStock: number;
lowStockCount: number;
outOfStockCount: number;
expiringSoon: number;
// Production metrics
productsSoldToday: number;
productsSoldTrend: number;
// Data freshness
lastUpdated: string;
}
interface AggregatedDashboardData {
alerts?: AlertAnalytics;
orders?: OrdersDashboardSummary;
sales?: SalesAnalytics;
inventory?: InventoryDashboardSummary;
}
// Query Keys
export const dashboardKeys = {
all: ['dashboard'] as const,
stats: (tenantId: string) => [...dashboardKeys.all, 'stats', tenantId] as const,
inventory: (tenantId: string) => [...dashboardKeys.all, 'inventory', tenantId] as const,
} as const;
/**
* Fetch inventory dashboard summary
*/
export const useInventoryDashboard = (
tenantId: string,
options?: Omit<UseQueryOptions<InventoryDashboardSummary, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<InventoryDashboardSummary, ApiError>({
queryKey: dashboardKeys.inventory(tenantId),
queryFn: () => inventoryService.getDashboardSummary(tenantId),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Fetch alert analytics
*/
export const useAlertAnalytics = (
tenantId: string,
days: number = 7,
options?: Omit<UseQueryOptions<AlertAnalytics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<AlertAnalytics, ApiError>({
queryKey: ['alerts', 'analytics', tenantId, days],
queryFn: () => getAlertAnalytics(tenantId, days),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Calculate percentage change between two values
*/
function calculateTrend(current: number, previous: number): number {
if (previous === 0) return current > 0 ? 100 : 0;
return Math.round(((current - previous) / previous) * 100);
}
/**
* Calculate today's sales from sales records
*/
function calculateTodaySales(salesData?: SalesAnalytics): { amount: number; trend: number; productsSold: number; productsTrend: number } {
if (!salesData) {
return { amount: 0, trend: 0, productsSold: 0, productsTrend: 0 };
}
// Sales data should have today's revenue and comparison
const todayRevenue = salesData.total_revenue || 0;
const previousRevenue = salesData.previous_period_revenue || 0;
const todayUnits = salesData.total_units_sold || 0;
const previousUnits = salesData.previous_period_units_sold || 0;
return {
amount: todayRevenue,
trend: calculateTrend(todayRevenue, previousRevenue),
productsSold: todayUnits,
productsTrend: calculateTrend(todayUnits, previousUnits),
};
}
/**
* Calculate orders metrics
*/
function calculateOrdersMetrics(ordersData?: OrdersDashboardSummary): { pending: number; today: number; trend: number } {
if (!ordersData) {
return { pending: 0, today: 0, trend: 0 };
}
const pendingCount = ordersData.pending_orders_count || 0;
const todayCount = ordersData.orders_today_count || 0;
const yesterdayCount = ordersData.orders_yesterday_count || 0;
return {
pending: pendingCount,
today: todayCount,
trend: calculateTrend(todayCount, yesterdayCount),
};
}
/**
* Aggregate dashboard data from all services
*/
function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats {
const sales = calculateTodaySales(data.sales);
const orders = calculateOrdersMetrics(data.orders);
const criticalStockCount =
(data.inventory?.low_stock_count || 0) +
(data.inventory?.out_of_stock_count || 0);
return {
// Alerts
activeAlerts: data.alerts?.activeAlerts || 0,
criticalAlerts: data.alerts?.totalAlerts || 0,
// Orders
pendingOrders: orders.pending,
ordersToday: orders.today,
ordersTrend: orders.trend,
// Sales
salesToday: sales.amount,
salesTrend: sales.trend,
salesCurrency: '€', // Default to EUR for bakery
// Inventory
criticalStock: criticalStockCount,
lowStockCount: data.inventory?.low_stock_count || 0,
outOfStockCount: data.inventory?.out_of_stock_count || 0,
expiringSoon: data.inventory?.expiring_soon_count || 0,
// Products
productsSoldToday: sales.productsSold,
productsSoldTrend: sales.productsTrend,
// Metadata
lastUpdated: new Date().toISOString(),
};
}
/**
* Main hook to fetch aggregated dashboard statistics
* Combines data from multiple services into a single cohesive dashboard view
*/
export const useDashboardStats = (
tenantId: string,
options?: Omit<UseQueryOptions<DashboardStats, ApiError>, 'queryKey' | 'queryFn'>
) => {
// Get today's date range for sales
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
return useQuery<DashboardStats, ApiError>({
queryKey: dashboardKeys.stats(tenantId),
queryFn: async () => {
// Fetch all data in parallel
const [alertsData, ordersData, salesData, inventoryData] = await Promise.allSettled([
getAlertAnalytics(tenantId, 7),
// Note: OrdersService methods are static
import('../services/orders').then(({ OrdersService }) =>
OrdersService.getDashboardSummary(tenantId)
),
// Fetch today's sales with comparison to yesterday
import('../services/sales').then(({ salesService }) =>
salesService.getSalesAnalytics(tenantId, todayStr, todayStr)
),
inventoryService.getDashboardSummary(tenantId),
]);
// Extract data or use undefined for failed requests
const aggregatedData: AggregatedDashboardData = {
alerts: alertsData.status === 'fulfilled' ? alertsData.value : undefined,
orders: ordersData.status === 'fulfilled' ? ordersData.value : undefined,
sales: salesData.status === 'fulfilled' ? salesData.value : undefined,
inventory: inventoryData.status === 'fulfilled' ? inventoryData.value : undefined,
};
// Log any failures for debugging
if (alertsData.status === 'rejected') {
console.warn('[Dashboard] Failed to fetch alerts:', alertsData.reason);
}
if (ordersData.status === 'rejected') {
console.warn('[Dashboard] Failed to fetch orders:', ordersData.reason);
}
if (salesData.status === 'rejected') {
console.warn('[Dashboard] Failed to fetch sales:', salesData.reason);
}
if (inventoryData.status === 'rejected') {
console.warn('[Dashboard] Failed to fetch inventory:', inventoryData.reason);
}
return aggregateDashboardStats(aggregatedData);
},
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
refetchInterval: 60 * 1000, // Auto-refresh every minute
retry: 2, // Retry failed requests twice
retryDelay: 1000, // Wait 1s between retries
...options,
});
};

View File

@@ -0,0 +1,259 @@
/**
* Purchase Orders React Query hooks
* Handles data fetching and mutations for purchase orders
*/
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { ApiError } from '../client/apiClient';
import type {
PurchaseOrderSummary,
PurchaseOrderDetail,
PurchaseOrderSearchParams,
PurchaseOrderUpdateData,
PurchaseOrderStatus
} from '../services/purchase_orders';
import {
listPurchaseOrders,
getPurchaseOrder,
getPendingApprovalPurchaseOrders,
getPurchaseOrdersByStatus,
updatePurchaseOrder,
approvePurchaseOrder,
rejectPurchaseOrder,
bulkApprovePurchaseOrders,
deletePurchaseOrder
} from '../services/purchase_orders';
// Query Keys
export const purchaseOrderKeys = {
all: ['purchase-orders'] as const,
lists: () => [...purchaseOrderKeys.all, 'list'] as const,
list: (tenantId: string, params?: PurchaseOrderSearchParams) =>
[...purchaseOrderKeys.lists(), tenantId, params] as const,
details: () => [...purchaseOrderKeys.all, 'detail'] as const,
detail: (tenantId: string, poId: string) =>
[...purchaseOrderKeys.details(), tenantId, poId] as const,
byStatus: (tenantId: string, status: PurchaseOrderStatus) =>
[...purchaseOrderKeys.lists(), tenantId, 'status', status] as const,
pendingApproval: (tenantId: string) =>
[...purchaseOrderKeys.lists(), tenantId, 'pending-approval'] as const,
} as const;
/**
* Hook to list purchase orders with optional filters
*/
export const usePurchaseOrders = (
tenantId: string,
params?: PurchaseOrderSearchParams,
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PurchaseOrderSummary[], ApiError>({
queryKey: purchaseOrderKeys.list(tenantId, params),
queryFn: () => listPurchaseOrders(tenantId, params),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Hook to get pending approval purchase orders
*/
export const usePendingApprovalPurchaseOrders = (
tenantId: string,
limit: number = 50,
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PurchaseOrderSummary[], ApiError>({
queryKey: purchaseOrderKeys.pendingApproval(tenantId),
queryFn: () => getPendingApprovalPurchaseOrders(tenantId, limit),
enabled: !!tenantId,
staleTime: 15 * 1000, // 15 seconds - more frequent for pending approvals
refetchInterval: 30 * 1000, // Auto-refetch every 30 seconds
...options,
});
};
/**
* Hook to get purchase orders by status
*/
export const usePurchaseOrdersByStatus = (
tenantId: string,
status: PurchaseOrderStatus,
limit: number = 50,
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PurchaseOrderSummary[], ApiError>({
queryKey: purchaseOrderKeys.byStatus(tenantId, status),
queryFn: () => getPurchaseOrdersByStatus(tenantId, status, limit),
enabled: !!tenantId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Hook to get a single purchase order detail
*/
export const usePurchaseOrder = (
tenantId: string,
poId: string,
options?: Omit<UseQueryOptions<PurchaseOrderDetail, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<PurchaseOrderDetail, ApiError>({
queryKey: purchaseOrderKeys.detail(tenantId, poId),
queryFn: () => getPurchaseOrder(tenantId, poId),
enabled: !!tenantId && !!poId,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Hook to update a purchase order
*/
export const useUpdatePurchaseOrder = (
options?: UseMutationOptions<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; data: PurchaseOrderUpdateData }
>
) => {
const queryClient = useQueryClient();
return useMutation<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; data: PurchaseOrderUpdateData }
>({
mutationFn: ({ tenantId, poId, data }) => updatePurchaseOrder(tenantId, poId, data),
onSuccess: (data, variables) => {
// Invalidate and refetch related queries
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
queryClient.invalidateQueries({
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
});
},
...options,
});
};
/**
* Hook to approve a purchase order
*/
export const useApprovePurchaseOrder = (
options?: UseMutationOptions<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; notes?: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; notes?: string }
>({
mutationFn: ({ tenantId, poId, notes }) => approvePurchaseOrder(tenantId, poId, notes),
onSuccess: (data, variables) => {
// Invalidate pending approvals list
queryClient.invalidateQueries({
queryKey: purchaseOrderKeys.pendingApproval(variables.tenantId)
});
// Invalidate all lists
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
// Invalidate detail
queryClient.invalidateQueries({
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
});
},
...options,
});
};
/**
* Hook to reject a purchase order
*/
export const useRejectPurchaseOrder = (
options?: UseMutationOptions<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; reason: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
PurchaseOrderDetail,
ApiError,
{ tenantId: string; poId: string; reason: string }
>({
mutationFn: ({ tenantId, poId, reason }) => rejectPurchaseOrder(tenantId, poId, reason),
onSuccess: (data, variables) => {
// Invalidate pending approvals list
queryClient.invalidateQueries({
queryKey: purchaseOrderKeys.pendingApproval(variables.tenantId)
});
// Invalidate all lists
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
// Invalidate detail
queryClient.invalidateQueries({
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
});
},
...options,
});
};
/**
* Hook to bulk approve purchase orders
*/
export const useBulkApprovePurchaseOrders = (
options?: UseMutationOptions<
PurchaseOrderDetail[],
ApiError,
{ tenantId: string; poIds: string[]; notes?: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
PurchaseOrderDetail[],
ApiError,
{ tenantId: string; poIds: string[]; notes?: string }
>({
mutationFn: ({ tenantId, poIds, notes }) => bulkApprovePurchaseOrders(tenantId, poIds, notes),
onSuccess: (data, variables) => {
// Invalidate all PO queries
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.all });
},
...options,
});
};
/**
* Hook to delete a purchase order
*/
export const useDeletePurchaseOrder = (
options?: UseMutationOptions<
{ message: string },
ApiError,
{ tenantId: string; poId: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
{ message: string },
ApiError,
{ tenantId: string; poId: string }
>({
mutationFn: ({ tenantId, poId }) => deletePurchaseOrder(tenantId, poId),
onSuccess: (data, variables) => {
// Invalidate all PO queries
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.all });
},
...options,
});
};

View File

@@ -0,0 +1,125 @@
/**
* Alert Analytics API Client
* Handles all API calls for alert analytics and interaction tracking
*/
import { apiClient } from '../client';
export interface AlertTrendData {
date: string;
count: number;
urgentCount: number;
highCount: number;
mediumCount: number;
lowCount: number;
}
export interface AlertCategory {
category: string;
count: number;
percentage: number;
}
export interface AlertAnalytics {
trends: AlertTrendData[];
averageResponseTime: number;
topCategories: AlertCategory[];
totalAlerts: number;
resolvedAlerts: number;
activeAlerts: number;
resolutionRate: number;
predictedDailyAverage: number;
busiestDay: string;
}
export interface AlertInteraction {
alert_id: string;
interaction_type: 'acknowledged' | 'resolved' | 'snoozed' | 'dismissed';
metadata?: Record<string, any>;
}
export interface InteractionResponse {
id: string;
alert_id: string;
interaction_type: string;
interacted_at: string;
response_time_seconds: number;
}
export interface BatchInteractionResponse {
created_count: number;
interactions: Array<{
id: string;
alert_id: string;
interaction_type: string;
interacted_at: string;
}>;
}
/**
* Track a single alert interaction
*/
export async function trackAlertInteraction(
tenantId: string,
alertId: string,
interactionType: 'acknowledged' | 'resolved' | 'snoozed' | 'dismissed',
metadata?: Record<string, any>
): Promise<InteractionResponse> {
return apiClient.post<InteractionResponse>(
`/tenants/${tenantId}/alerts/${alertId}/interactions`,
{
alert_id: alertId,
interaction_type: interactionType,
metadata
}
);
}
/**
* Track multiple alert interactions in batch
*/
export async function trackAlertInteractionsBatch(
tenantId: string,
interactions: AlertInteraction[]
): Promise<BatchInteractionResponse> {
return apiClient.post<BatchInteractionResponse>(
`/tenants/${tenantId}/alerts/interactions/batch`,
{
interactions
}
);
}
/**
* Get comprehensive alert analytics
*/
export async function getAlertAnalytics(
tenantId: string,
days: number = 7
): Promise<AlertAnalytics> {
console.log('[getAlertAnalytics] Calling API:', `/tenants/${tenantId}/alerts/analytics`, 'with days:', days);
const data = await apiClient.get<AlertAnalytics>(
`/tenants/${tenantId}/alerts/analytics`,
{
params: { days }
}
);
console.log('[getAlertAnalytics] Received data:', data);
console.log('[getAlertAnalytics] Data type:', typeof data);
return data; // apiClient.get() already returns data, not response.data
}
/**
* Get alert trends only
*/
export async function getAlertTrends(
tenantId: string,
days: number = 7
): Promise<AlertTrendData[]> {
return apiClient.get<AlertTrendData[]>(
`/tenants/${tenantId}/alerts/analytics/trends`,
{
params: { days }
}
);
}

View File

@@ -84,7 +84,7 @@ export class OrdersService {
*/
static async createOrder(orderData: OrderCreate): Promise<OrderResponse> {
const { tenant_id, ...data } = orderData;
return apiClient.post<OrderResponse>(`/tenants/${tenant_id}/orders/orders`, data);
return apiClient.post<OrderResponse>(`/tenants/${tenant_id}/orders`, data);
}
/**
@@ -92,7 +92,7 @@ export class OrdersService {
* GET /tenants/{tenant_id}/orders/{order_id}
*/
static async getOrder(tenantId: string, orderId: string): Promise<OrderResponse> {
return apiClient.get<OrderResponse>(`/tenants/${tenantId}/orders/orders/${orderId}`);
return apiClient.get<OrderResponse>(`/tenants/${tenantId}/orders/${orderId}`);
}
/**
@@ -117,7 +117,7 @@ export class OrdersService {
queryParams.append('end_date', end_date);
}
return apiClient.get<OrderResponse[]>(`/tenants/${tenant_id}/orders/orders?${queryParams.toString()}`);
return apiClient.get<OrderResponse[]>(`/tenants/${tenant_id}/orders?${queryParams.toString()}`);
}
/**

View File

@@ -381,7 +381,7 @@ export class ProductionService {
}
async getProductionRequirements(tenantId: string, date: string): Promise<any> {
return apiClient.get(`${this.baseUrl}/${tenantId}/production/dashboard/requirements/${date}`);
return apiClient.get(`${this.baseUrl}/${tenantId}/production/dashboard/requirements?date=${date}`);
}
async getCapacityOverview(tenantId: string, date?: string): Promise<any> {

View File

@@ -0,0 +1,239 @@
/**
* Purchase Orders API Client
* Handles all API calls for purchase orders in the suppliers service
*/
import { apiClient } from '../client';
export type PurchaseOrderStatus =
| 'DRAFT'
| 'PENDING_APPROVAL'
| 'APPROVED'
| 'SENT_TO_SUPPLIER'
| 'CONFIRMED'
| 'RECEIVED'
| 'COMPLETED'
| 'CANCELLED'
| 'DISPUTED';
export type PurchaseOrderPriority = 'urgent' | 'high' | 'normal' | 'low';
export interface PurchaseOrderItem {
id: string;
inventory_product_id: string;
product_code?: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: string; // Decimal as string
line_total: string; // Decimal as string
received_quantity: number;
remaining_quantity: number;
quality_requirements?: string;
item_notes?: string;
}
export interface SupplierSummary {
id: string;
name: string;
supplier_code: string;
supplier_type: string;
status: string;
contact_person?: string;
email?: string;
phone?: string;
}
export interface PurchaseOrderSummary {
id: string;
po_number: string;
supplier_id: string;
supplier_name?: string;
status: PurchaseOrderStatus;
priority: PurchaseOrderPriority;
order_date: string;
required_delivery_date?: string;
total_amount: string; // Decimal as string
currency: string;
created_at: string;
}
export interface PurchaseOrderDetail extends PurchaseOrderSummary {
reference_number?: string;
estimated_delivery_date?: string;
// Financial information
subtotal: string;
tax_amount: string;
shipping_cost: string;
discount_amount: string;
// Delivery information
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
// Approval workflow
requires_approval: boolean;
approved_by?: string;
approved_at?: string;
rejection_reason?: string;
// Communication tracking
sent_to_supplier_at?: string;
supplier_confirmation_date?: string;
supplier_reference?: string;
// Additional information
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
// Audit fields
updated_at: string;
created_by: string;
updated_by: string;
// Related data
supplier?: SupplierSummary;
items?: PurchaseOrderItem[];
}
export interface PurchaseOrderSearchParams {
supplier_id?: string;
status?: PurchaseOrderStatus;
priority?: PurchaseOrderPriority;
date_from?: string; // YYYY-MM-DD
date_to?: string; // YYYY-MM-DD
search_term?: string;
limit?: number;
offset?: number;
}
export interface PurchaseOrderUpdateData {
status?: PurchaseOrderStatus;
priority?: PurchaseOrderPriority;
notes?: string;
rejection_reason?: string;
internal_notes?: string;
}
/**
* Get list of purchase orders with optional filters
*/
export async function listPurchaseOrders(
tenantId: string,
params?: PurchaseOrderSearchParams
): Promise<PurchaseOrderSummary[]> {
return apiClient.get<PurchaseOrderSummary[]>(
`/tenants/${tenantId}/purchase-orders`,
{ params }
);
}
/**
* Get purchase orders by status
*/
export async function getPurchaseOrdersByStatus(
tenantId: string,
status: PurchaseOrderStatus,
limit: number = 50
): Promise<PurchaseOrderSummary[]> {
return listPurchaseOrders(tenantId, { status, limit });
}
/**
* Get pending approval purchase orders
*/
export async function getPendingApprovalPurchaseOrders(
tenantId: string,
limit: number = 50
): Promise<PurchaseOrderSummary[]> {
return getPurchaseOrdersByStatus(tenantId, 'PENDING_APPROVAL', limit);
}
/**
* Get a single purchase order by ID with full details
*/
export async function getPurchaseOrder(
tenantId: string,
poId: string
): Promise<PurchaseOrderDetail> {
return apiClient.get<PurchaseOrderDetail>(
`/tenants/${tenantId}/purchase-orders/${poId}`
);
}
/**
* Update purchase order
*/
export async function updatePurchaseOrder(
tenantId: string,
poId: string,
data: PurchaseOrderUpdateData
): Promise<PurchaseOrderDetail> {
return apiClient.put<PurchaseOrderDetail>(
`/tenants/${tenantId}/purchase-orders/${poId}`,
data
);
}
/**
* Approve a purchase order
*/
export async function approvePurchaseOrder(
tenantId: string,
poId: string,
notes?: string
): Promise<PurchaseOrderDetail> {
return apiClient.post<PurchaseOrderDetail>(
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
{
action: 'approve',
notes: notes || 'Approved from dashboard'
}
);
}
/**
* Reject a purchase order
*/
export async function rejectPurchaseOrder(
tenantId: string,
poId: string,
reason: string
): Promise<PurchaseOrderDetail> {
return apiClient.post<PurchaseOrderDetail>(
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
{
action: 'reject',
notes: reason
}
);
}
/**
* Bulk approve purchase orders
*/
export async function bulkApprovePurchaseOrders(
tenantId: string,
poIds: string[],
notes?: string
): Promise<PurchaseOrderDetail[]> {
const approvalPromises = poIds.map(poId =>
approvePurchaseOrder(tenantId, poId, notes)
);
return Promise.all(approvalPromises);
}
/**
* Delete purchase order
*/
export async function deletePurchaseOrder(
tenantId: string,
poId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`/tenants/${tenantId}/purchase-orders/${poId}`
);
}

View File

@@ -104,6 +104,19 @@ class SuppliersService {
);
}
async getSupplierProducts(
tenantId: string,
supplierId: string,
isActive: boolean = true
): Promise<Array<{ inventory_product_id: string }>> {
const params = new URLSearchParams();
params.append('is_active', isActive.toString());
return apiClient.get<Array<{ inventory_product_id: string }>>(
`${this.baseUrl}/${tenantId}/suppliers/suppliers/${supplierId}/products?${params.toString()}`
);
}
// ===================================================================
// ATOMIC: Purchase Orders CRUD
// Backend: services/suppliers/app/api/purchase_orders.py