Add frontend procurement implementation
This commit is contained in:
@@ -18,6 +18,19 @@ import {
|
|||||||
GetCustomersParams,
|
GetCustomersParams,
|
||||||
UpdateOrderStatusParams,
|
UpdateOrderStatusParams,
|
||||||
GetDemandRequirementsParams,
|
GetDemandRequirementsParams,
|
||||||
|
// Procurement types
|
||||||
|
ProcurementPlanResponse,
|
||||||
|
ProcurementPlanCreate,
|
||||||
|
ProcurementPlanUpdate,
|
||||||
|
ProcurementRequirementResponse,
|
||||||
|
ProcurementRequirementUpdate,
|
||||||
|
ProcurementDashboardData,
|
||||||
|
GeneratePlanRequest,
|
||||||
|
GeneratePlanResponse,
|
||||||
|
PaginatedProcurementPlans,
|
||||||
|
GetProcurementPlansParams,
|
||||||
|
GetPlanRequirementsParams,
|
||||||
|
UpdatePlanStatusParams,
|
||||||
} from '../types/orders';
|
} from '../types/orders';
|
||||||
import { ApiError } from '../client/apiClient';
|
import { ApiError } from '../client/apiClient';
|
||||||
|
|
||||||
@@ -42,6 +55,17 @@ export const ordersKeys = {
|
|||||||
|
|
||||||
// Status
|
// Status
|
||||||
status: (tenantId: string) => [...ordersKeys.all, 'status', tenantId] as const,
|
status: (tenantId: string) => [...ordersKeys.all, 'status', tenantId] as const,
|
||||||
|
|
||||||
|
// Procurement
|
||||||
|
procurement: () => [...ordersKeys.all, 'procurement'] as const,
|
||||||
|
procurementPlans: (params: GetProcurementPlansParams) => [...ordersKeys.procurement(), 'plans', params] as const,
|
||||||
|
procurementPlan: (tenantId: string, planId: string) => [...ordersKeys.procurement(), 'plan', tenantId, planId] as const,
|
||||||
|
procurementPlanByDate: (tenantId: string, date: string) => [...ordersKeys.procurement(), 'plan-by-date', tenantId, date] as const,
|
||||||
|
currentProcurementPlan: (tenantId: string) => [...ordersKeys.procurement(), 'current-plan', tenantId] as const,
|
||||||
|
procurementDashboard: (tenantId: string) => [...ordersKeys.procurement(), 'dashboard', tenantId] as const,
|
||||||
|
planRequirements: (params: GetPlanRequirementsParams) => [...ordersKeys.procurement(), 'requirements', params] as const,
|
||||||
|
criticalRequirements: (tenantId: string) => [...ordersKeys.procurement(), 'critical-requirements', tenantId] as const,
|
||||||
|
procurementHealth: (tenantId: string) => [...ordersKeys.procurement(), 'health', tenantId] as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ===== Order Queries =====
|
// ===== Order Queries =====
|
||||||
@@ -329,4 +353,195 @@ export const useInvalidateOrders = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Procurement Queries =====
|
||||||
|
|
||||||
|
export const useProcurementPlans = (
|
||||||
|
params: GetProcurementPlansParams,
|
||||||
|
options?: Omit<UseQueryOptions<PaginatedProcurementPlans, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<PaginatedProcurementPlans, ApiError>({
|
||||||
|
queryKey: ordersKeys.procurementPlans(params),
|
||||||
|
queryFn: () => OrdersService.getProcurementPlans(params),
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
enabled: !!params.tenant_id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProcurementPlan = (
|
||||||
|
tenantId: string,
|
||||||
|
planId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||||
|
queryKey: ordersKeys.procurementPlan(tenantId, planId),
|
||||||
|
queryFn: () => OrdersService.getProcurementPlanById(tenantId, planId),
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
enabled: !!tenantId && !!planId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProcurementPlanByDate = (
|
||||||
|
tenantId: string,
|
||||||
|
planDate: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||||
|
queryKey: ordersKeys.procurementPlanByDate(tenantId, planDate),
|
||||||
|
queryFn: () => OrdersService.getProcurementPlanByDate(tenantId, planDate),
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
enabled: !!tenantId && !!planDate,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCurrentProcurementPlan = (
|
||||||
|
tenantId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||||
|
queryKey: ordersKeys.currentProcurementPlan(tenantId),
|
||||||
|
queryFn: () => OrdersService.getCurrentProcurementPlan(tenantId),
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 minute
|
||||||
|
enabled: !!tenantId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProcurementDashboard = (
|
||||||
|
tenantId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementDashboardData | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementDashboardData | null, ApiError>({
|
||||||
|
queryKey: ordersKeys.procurementDashboard(tenantId),
|
||||||
|
queryFn: () => OrdersService.getProcurementDashboard(tenantId),
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
enabled: !!tenantId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePlanRequirements = (
|
||||||
|
params: GetPlanRequirementsParams,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||||
|
queryKey: ordersKeys.planRequirements(params),
|
||||||
|
queryFn: () => OrdersService.getPlanRequirements(params),
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
enabled: !!params.tenant_id && !!params.plan_id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCriticalRequirements = (
|
||||||
|
tenantId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||||
|
queryKey: ordersKeys.criticalRequirements(tenantId),
|
||||||
|
queryFn: () => OrdersService.getCriticalRequirements(tenantId),
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 minute
|
||||||
|
enabled: !!tenantId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProcurementHealth = (
|
||||||
|
tenantId: string,
|
||||||
|
options?: Omit<UseQueryOptions<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }, ApiError>({
|
||||||
|
queryKey: ordersKeys.procurementHealth(tenantId),
|
||||||
|
queryFn: () => OrdersService.getProcurementHealth(tenantId),
|
||||||
|
staleTime: 30 * 1000, // 30 seconds
|
||||||
|
enabled: !!tenantId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Procurement Mutations =====
|
||||||
|
|
||||||
|
export const useGenerateProcurementPlan = (
|
||||||
|
options?: UseMutationOptions<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>({
|
||||||
|
mutationFn: ({ tenantId, request }) => OrdersService.generateProcurementPlan(tenantId, request),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Invalidate all procurement queries for this tenant
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersKeys.procurement(),
|
||||||
|
predicate: (query) => {
|
||||||
|
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If plan was generated successfully, cache it
|
||||||
|
if (data.success && data.plan) {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
ordersKeys.procurementPlan(variables.tenantId, data.plan.id),
|
||||||
|
data.plan
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateProcurementPlanStatus = (
|
||||||
|
options?: UseMutationOptions<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>({
|
||||||
|
mutationFn: (params) => OrdersService.updateProcurementPlanStatus(params),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Update the specific plan in cache
|
||||||
|
queryClient.setQueryData(
|
||||||
|
ordersKeys.procurementPlan(variables.tenant_id, variables.plan_id),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalidate plans list
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersKeys.procurement(),
|
||||||
|
predicate: (query) => {
|
||||||
|
const queryKey = query.queryKey as string[];
|
||||||
|
return queryKey.includes('plans') &&
|
||||||
|
JSON.stringify(queryKey).includes(variables.tenant_id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate dashboard
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersKeys.procurementDashboard(variables.tenant_id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTriggerDailyScheduler = (
|
||||||
|
options?: UseMutationOptions<{ success: boolean; message: string; tenant_id: string }, ApiError, string>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ success: boolean; message: string; tenant_id: string }, ApiError, string>({
|
||||||
|
mutationFn: (tenantId) => OrdersService.triggerDailyScheduler(tenantId),
|
||||||
|
onSuccess: (data, tenantId) => {
|
||||||
|
// Invalidate all procurement data for this tenant
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersKeys.procurement(),
|
||||||
|
predicate: (query) => {
|
||||||
|
return JSON.stringify(query.queryKey).includes(tenantId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
@@ -269,6 +269,30 @@ export type {
|
|||||||
GetCustomersParams,
|
GetCustomersParams,
|
||||||
UpdateOrderStatusParams,
|
UpdateOrderStatusParams,
|
||||||
GetDemandRequirementsParams,
|
GetDemandRequirementsParams,
|
||||||
|
// Procurement types
|
||||||
|
ProcurementPlanType,
|
||||||
|
ProcurementStrategy,
|
||||||
|
RiskLevel,
|
||||||
|
RequirementStatus,
|
||||||
|
PlanStatus,
|
||||||
|
DeliveryStatus,
|
||||||
|
ProcurementRequirementBase,
|
||||||
|
ProcurementRequirementCreate,
|
||||||
|
ProcurementRequirementUpdate,
|
||||||
|
ProcurementRequirementResponse,
|
||||||
|
ProcurementPlanBase,
|
||||||
|
ProcurementPlanCreate,
|
||||||
|
ProcurementPlanUpdate,
|
||||||
|
ProcurementPlanResponse,
|
||||||
|
ProcurementSummary,
|
||||||
|
ProcurementDashboardData,
|
||||||
|
GeneratePlanRequest,
|
||||||
|
GeneratePlanResponse,
|
||||||
|
PaginatedProcurementPlans,
|
||||||
|
ForecastRequest,
|
||||||
|
GetProcurementPlansParams,
|
||||||
|
GetPlanRequirementsParams,
|
||||||
|
UpdatePlanStatusParams,
|
||||||
} from './types/orders';
|
} from './types/orders';
|
||||||
|
|
||||||
// Hooks - Auth
|
// Hooks - Auth
|
||||||
@@ -512,6 +536,18 @@ export {
|
|||||||
useCreateCustomer,
|
useCreateCustomer,
|
||||||
useUpdateCustomer,
|
useUpdateCustomer,
|
||||||
useInvalidateOrders,
|
useInvalidateOrders,
|
||||||
|
// Procurement hooks
|
||||||
|
useProcurementPlans,
|
||||||
|
useProcurementPlan,
|
||||||
|
useProcurementPlanByDate,
|
||||||
|
useCurrentProcurementPlan,
|
||||||
|
useProcurementDashboard,
|
||||||
|
usePlanRequirements,
|
||||||
|
useCriticalRequirements,
|
||||||
|
useProcurementHealth,
|
||||||
|
useGenerateProcurementPlan,
|
||||||
|
useUpdateProcurementPlanStatus,
|
||||||
|
useTriggerDailyScheduler,
|
||||||
ordersKeys,
|
ordersKeys,
|
||||||
} from './hooks/orders';
|
} from './hooks/orders';
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ import {
|
|||||||
GetCustomersParams,
|
GetCustomersParams,
|
||||||
UpdateOrderStatusParams,
|
UpdateOrderStatusParams,
|
||||||
GetDemandRequirementsParams,
|
GetDemandRequirementsParams,
|
||||||
|
// Procurement types
|
||||||
|
ProcurementPlanResponse,
|
||||||
|
ProcurementPlanCreate,
|
||||||
|
ProcurementPlanUpdate,
|
||||||
|
ProcurementRequirementResponse,
|
||||||
|
ProcurementRequirementUpdate,
|
||||||
|
ProcurementDashboardData,
|
||||||
|
GeneratePlanRequest,
|
||||||
|
GeneratePlanResponse,
|
||||||
|
PaginatedProcurementPlans,
|
||||||
|
GetProcurementPlansParams,
|
||||||
|
GetPlanRequirementsParams,
|
||||||
|
UpdatePlanStatusParams,
|
||||||
} from '../types/orders';
|
} from '../types/orders';
|
||||||
|
|
||||||
export class OrdersService {
|
export class OrdersService {
|
||||||
@@ -168,6 +181,127 @@ export class OrdersService {
|
|||||||
static async getServiceStatus(tenantId: string): Promise<ServiceStatus> {
|
static async getServiceStatus(tenantId: string): Promise<ServiceStatus> {
|
||||||
return apiClient.get<ServiceStatus>(`/tenants/${tenantId}/orders/status`);
|
return apiClient.get<ServiceStatus>(`/tenants/${tenantId}/orders/status`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Procurement Planning Endpoints =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current procurement plan for today
|
||||||
|
* GET /tenants/{tenant_id}/procurement/plans/current
|
||||||
|
*/
|
||||||
|
static async getCurrentProcurementPlan(tenantId: string): Promise<ProcurementPlanResponse | null> {
|
||||||
|
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/procurement/plans/current`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get procurement plan by specific date
|
||||||
|
* GET /tenants/{tenant_id}/procurement/plans/date/{plan_date}
|
||||||
|
*/
|
||||||
|
static async getProcurementPlanByDate(tenantId: string, planDate: string): Promise<ProcurementPlanResponse | null> {
|
||||||
|
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/procurement/plans/date/${planDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get procurement plan by ID
|
||||||
|
* GET /tenants/{tenant_id}/procurement/plans/id/{plan_id}
|
||||||
|
*/
|
||||||
|
static async getProcurementPlanById(tenantId: string, planId: string): Promise<ProcurementPlanResponse | null> {
|
||||||
|
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/procurement/plans/id/${planId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List procurement plans with filtering
|
||||||
|
* GET /tenants/{tenant_id}/procurement/plans/
|
||||||
|
*/
|
||||||
|
static async getProcurementPlans(params: GetProcurementPlansParams): Promise<PaginatedProcurementPlans> {
|
||||||
|
const { tenant_id, status, start_date, end_date, limit = 50, offset = 0 } = params;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
limit: limit.toString(),
|
||||||
|
offset: offset.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status) queryParams.append('status', status);
|
||||||
|
if (start_date) queryParams.append('start_date', start_date);
|
||||||
|
if (end_date) queryParams.append('end_date', end_date);
|
||||||
|
|
||||||
|
return apiClient.get<PaginatedProcurementPlans>(
|
||||||
|
`/tenants/${tenant_id}/procurement/plans?${queryParams.toString()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new procurement plan
|
||||||
|
* POST /tenants/{tenant_id}/procurement/plans/generate
|
||||||
|
*/
|
||||||
|
static async generateProcurementPlan(tenantId: string, request: GeneratePlanRequest): Promise<GeneratePlanResponse> {
|
||||||
|
return apiClient.post<GeneratePlanResponse>(`/tenants/${tenantId}/procurement/plans/generate`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update procurement plan status
|
||||||
|
* PUT /tenants/{tenant_id}/procurement/plans/{plan_id}/status
|
||||||
|
*/
|
||||||
|
static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise<ProcurementPlanResponse> {
|
||||||
|
const { tenant_id, plan_id, status } = params;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({ status });
|
||||||
|
|
||||||
|
return apiClient.put<ProcurementPlanResponse>(
|
||||||
|
`/tenants/${tenant_id}/procurement/plans/${plan_id}/status?${queryParams.toString()}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get procurement dashboard data
|
||||||
|
* GET /tenants/{tenant_id}/procurement/dashboard
|
||||||
|
*/
|
||||||
|
static async getProcurementDashboard(tenantId: string): Promise<ProcurementDashboardData | null> {
|
||||||
|
return apiClient.get<ProcurementDashboardData | null>(`/tenants/${tenantId}/procurement/dashboard`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get requirements for a specific plan
|
||||||
|
* GET /tenants/{tenant_id}/procurement/plans/{plan_id}/requirements
|
||||||
|
*/
|
||||||
|
static async getPlanRequirements(params: GetPlanRequirementsParams): Promise<ProcurementRequirementResponse[]> {
|
||||||
|
const { tenant_id, plan_id, status, priority } = params;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (status) queryParams.append('status', status);
|
||||||
|
if (priority) queryParams.append('priority', priority);
|
||||||
|
|
||||||
|
const url = `/tenants/${tenant_id}/procurement/plans/${plan_id}/requirements${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
||||||
|
|
||||||
|
return apiClient.get<ProcurementRequirementResponse[]>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get critical requirements across all plans
|
||||||
|
* GET /tenants/{tenant_id}/procurement/requirements/critical
|
||||||
|
*/
|
||||||
|
static async getCriticalRequirements(tenantId: string): Promise<ProcurementRequirementResponse[]> {
|
||||||
|
return apiClient.get<ProcurementRequirementResponse[]>(`/tenants/${tenantId}/procurement/requirements/critical`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger daily scheduler manually
|
||||||
|
* POST /tenants/{tenant_id}/procurement/scheduler/trigger
|
||||||
|
*/
|
||||||
|
static async triggerDailyScheduler(tenantId: string): Promise<{ success: boolean; message: string; tenant_id: string }> {
|
||||||
|
return apiClient.post<{ success: boolean; message: string; tenant_id: string }>(
|
||||||
|
`/tenants/${tenantId}/procurement/scheduler/trigger`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get procurement service health
|
||||||
|
* GET /tenants/{tenant_id}/procurement/health
|
||||||
|
*/
|
||||||
|
static async getProcurementHealth(tenantId: string): Promise<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }> {
|
||||||
|
return apiClient.get<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }>(`/tenants/${tenantId}/procurement/health`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrdersService;
|
export default OrdersService;
|
||||||
@@ -280,4 +280,243 @@ export interface UpdateOrderStatusParams {
|
|||||||
export interface GetDemandRequirementsParams {
|
export interface GetDemandRequirementsParams {
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
target_date: string;
|
target_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Procurement Types =====
|
||||||
|
|
||||||
|
export type ProcurementPlanType = 'regular' | 'emergency' | 'seasonal';
|
||||||
|
export type ProcurementStrategy = 'just_in_time' | 'bulk' | 'mixed';
|
||||||
|
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
export type RequirementStatus = 'pending' | 'approved' | 'ordered' | 'partially_received' | 'received' | 'cancelled';
|
||||||
|
export type PlanStatus = 'draft' | 'pending_approval' | 'approved' | 'in_execution' | 'completed' | 'cancelled';
|
||||||
|
export type DeliveryStatus = 'pending' | 'in_transit' | 'delivered' | 'delayed' | 'cancelled';
|
||||||
|
|
||||||
|
// Procurement Requirement Types
|
||||||
|
export interface ProcurementRequirementBase {
|
||||||
|
product_id: string;
|
||||||
|
product_name: string;
|
||||||
|
product_sku?: string;
|
||||||
|
product_category?: string;
|
||||||
|
product_type: string;
|
||||||
|
required_quantity: number;
|
||||||
|
unit_of_measure: string;
|
||||||
|
safety_stock_quantity: number;
|
||||||
|
total_quantity_needed: number;
|
||||||
|
current_stock_level: number;
|
||||||
|
reserved_stock: number;
|
||||||
|
available_stock: number;
|
||||||
|
net_requirement: number;
|
||||||
|
order_demand: number;
|
||||||
|
production_demand: number;
|
||||||
|
forecast_demand: number;
|
||||||
|
buffer_demand: number;
|
||||||
|
required_by_date: string;
|
||||||
|
lead_time_buffer_days: number;
|
||||||
|
suggested_order_date: string;
|
||||||
|
latest_order_date: string;
|
||||||
|
priority: PriorityLevel;
|
||||||
|
risk_level: RiskLevel;
|
||||||
|
preferred_supplier_id?: string;
|
||||||
|
backup_supplier_id?: string;
|
||||||
|
supplier_name?: string;
|
||||||
|
supplier_lead_time_days?: number;
|
||||||
|
minimum_order_quantity?: number;
|
||||||
|
estimated_unit_cost?: number;
|
||||||
|
estimated_total_cost?: number;
|
||||||
|
last_purchase_cost?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementRequirementCreate extends ProcurementRequirementBase {
|
||||||
|
special_requirements?: string;
|
||||||
|
storage_requirements?: string;
|
||||||
|
shelf_life_days?: number;
|
||||||
|
quality_specifications?: Record<string, any>;
|
||||||
|
procurement_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementRequirementUpdate {
|
||||||
|
status?: RequirementStatus;
|
||||||
|
priority?: PriorityLevel;
|
||||||
|
approved_quantity?: number;
|
||||||
|
approved_cost?: number;
|
||||||
|
purchase_order_id?: string;
|
||||||
|
purchase_order_number?: string;
|
||||||
|
ordered_quantity?: number;
|
||||||
|
expected_delivery_date?: string;
|
||||||
|
actual_delivery_date?: string;
|
||||||
|
received_quantity?: number;
|
||||||
|
delivery_status?: DeliveryStatus;
|
||||||
|
procurement_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementRequirementResponse extends ProcurementRequirementBase {
|
||||||
|
id: string;
|
||||||
|
plan_id: string;
|
||||||
|
requirement_number: string;
|
||||||
|
status: RequirementStatus;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
purchase_order_id?: string;
|
||||||
|
purchase_order_number?: string;
|
||||||
|
ordered_quantity: number;
|
||||||
|
ordered_at?: string;
|
||||||
|
expected_delivery_date?: string;
|
||||||
|
actual_delivery_date?: string;
|
||||||
|
received_quantity: number;
|
||||||
|
delivery_status: DeliveryStatus;
|
||||||
|
fulfillment_rate?: number;
|
||||||
|
on_time_delivery?: boolean;
|
||||||
|
quality_rating?: number;
|
||||||
|
approved_quantity?: number;
|
||||||
|
approved_cost?: number;
|
||||||
|
approved_at?: string;
|
||||||
|
approved_by?: string;
|
||||||
|
special_requirements?: string;
|
||||||
|
storage_requirements?: string;
|
||||||
|
shelf_life_days?: number;
|
||||||
|
quality_specifications?: Record<string, any>;
|
||||||
|
procurement_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procurement Plan Types
|
||||||
|
export interface ProcurementPlanBase {
|
||||||
|
plan_date: string;
|
||||||
|
plan_period_start: string;
|
||||||
|
plan_period_end: string;
|
||||||
|
planning_horizon_days: number;
|
||||||
|
plan_type: ProcurementPlanType;
|
||||||
|
priority: PriorityLevel;
|
||||||
|
business_model?: BusinessModel;
|
||||||
|
procurement_strategy: ProcurementStrategy;
|
||||||
|
safety_stock_buffer: number;
|
||||||
|
supply_risk_level: RiskLevel;
|
||||||
|
demand_forecast_confidence?: number;
|
||||||
|
seasonality_adjustment: number;
|
||||||
|
special_requirements?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementPlanCreate extends ProcurementPlanBase {
|
||||||
|
tenant_id: string;
|
||||||
|
requirements?: ProcurementRequirementCreate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementPlanUpdate {
|
||||||
|
status?: PlanStatus;
|
||||||
|
priority?: PriorityLevel;
|
||||||
|
approved_at?: string;
|
||||||
|
approved_by?: string;
|
||||||
|
execution_started_at?: string;
|
||||||
|
execution_completed_at?: string;
|
||||||
|
special_requirements?: string;
|
||||||
|
seasonal_adjustments?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementPlanResponse extends ProcurementPlanBase {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
plan_number: string;
|
||||||
|
status: PlanStatus;
|
||||||
|
total_requirements: number;
|
||||||
|
total_estimated_cost: number;
|
||||||
|
total_approved_cost: number;
|
||||||
|
cost_variance: number;
|
||||||
|
total_demand_orders: number;
|
||||||
|
total_demand_quantity: number;
|
||||||
|
total_production_requirements: number;
|
||||||
|
primary_suppliers_count: number;
|
||||||
|
backup_suppliers_count: number;
|
||||||
|
supplier_diversification_score?: number;
|
||||||
|
approved_at?: string;
|
||||||
|
approved_by?: string;
|
||||||
|
execution_started_at?: string;
|
||||||
|
execution_completed_at?: string;
|
||||||
|
fulfillment_rate?: number;
|
||||||
|
on_time_delivery_rate?: number;
|
||||||
|
cost_accuracy?: number;
|
||||||
|
quality_score?: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by?: string;
|
||||||
|
updated_by?: string;
|
||||||
|
requirements: ProcurementRequirementResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary and Dashboard Types
|
||||||
|
export interface ProcurementSummary {
|
||||||
|
total_plans: number;
|
||||||
|
active_plans: number;
|
||||||
|
total_requirements: number;
|
||||||
|
pending_requirements: number;
|
||||||
|
critical_requirements: number;
|
||||||
|
total_estimated_cost: number;
|
||||||
|
total_approved_cost: number;
|
||||||
|
cost_variance: number;
|
||||||
|
average_fulfillment_rate?: number;
|
||||||
|
average_on_time_delivery?: number;
|
||||||
|
top_suppliers: Record<string, any>[];
|
||||||
|
critical_items: Record<string, any>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcurementDashboardData {
|
||||||
|
current_plan?: ProcurementPlanResponse;
|
||||||
|
summary: ProcurementSummary;
|
||||||
|
upcoming_deliveries: Record<string, any>[];
|
||||||
|
overdue_requirements: Record<string, any>[];
|
||||||
|
low_stock_alerts: Record<string, any>[];
|
||||||
|
performance_metrics: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request and Response Types
|
||||||
|
export interface GeneratePlanRequest {
|
||||||
|
plan_date?: string;
|
||||||
|
force_regenerate: boolean;
|
||||||
|
planning_horizon_days: number;
|
||||||
|
include_safety_stock: boolean;
|
||||||
|
safety_stock_percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratePlanResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
plan?: ProcurementPlanResponse;
|
||||||
|
warnings: string[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedProcurementPlans {
|
||||||
|
plans: ProcurementPlanResponse[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
has_more: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForecastRequest {
|
||||||
|
target_date: string;
|
||||||
|
horizon_days: number;
|
||||||
|
include_confidence_intervals: boolean;
|
||||||
|
product_ids?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Parameter Types for Procurement
|
||||||
|
export interface GetProcurementPlansParams {
|
||||||
|
tenant_id: string;
|
||||||
|
status?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPlanRequirementsParams {
|
||||||
|
tenant_id: string;
|
||||||
|
plan_id: string;
|
||||||
|
status?: string;
|
||||||
|
priority?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePlanStatusParams {
|
||||||
|
tenant_id: string;
|
||||||
|
plan_id: string;
|
||||||
|
status: PlanStatus;
|
||||||
}
|
}
|
||||||
@@ -1,239 +1,209 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react';
|
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader } from 'lucide-react';
|
||||||
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
||||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||||
import { PageHeader } from '../../../../components/layout';
|
import { PageHeader } from '../../../../components/layout';
|
||||||
|
import {
|
||||||
|
useProcurementDashboard,
|
||||||
|
useProcurementPlans,
|
||||||
|
useCurrentProcurementPlan,
|
||||||
|
useCriticalRequirements,
|
||||||
|
useGenerateProcurementPlan,
|
||||||
|
useUpdateProcurementPlanStatus,
|
||||||
|
useTriggerDailyScheduler
|
||||||
|
} from '../../../../api';
|
||||||
|
import { useTenantStore } from '../../../../stores/tenant.store';
|
||||||
|
|
||||||
const ProcurementPage: React.FC = () => {
|
const ProcurementPage: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState('orders');
|
const [activeTab, setActiveTab] = useState('plans');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||||
const [selectedOrder, setSelectedOrder] = useState<typeof mockPurchaseOrders[0] | null>(null);
|
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
||||||
|
|
||||||
|
const { currentTenant } = useTenantStore();
|
||||||
|
const tenantId = currentTenant?.id || '';
|
||||||
|
|
||||||
const mockPurchaseOrders = [
|
// Real API data hooks
|
||||||
{
|
const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId);
|
||||||
id: 'PO-2024-001',
|
const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({
|
||||||
supplier: 'Molinos del Sur',
|
tenant_id: tenantId,
|
||||||
status: 'pending',
|
limit: 50,
|
||||||
orderDate: '2024-01-25',
|
offset: 0
|
||||||
deliveryDate: '2024-01-28',
|
});
|
||||||
totalAmount: 1250.00,
|
const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId);
|
||||||
items: [
|
const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId);
|
||||||
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
|
|
||||||
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
|
const generatePlanMutation = useGenerateProcurementPlan();
|
||||||
],
|
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
|
||||||
paymentStatus: 'pending',
|
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
||||||
notes: 'Entrega en horario de mañana',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'PO-2024-002',
|
|
||||||
supplier: 'Levaduras SA',
|
|
||||||
status: 'delivered',
|
|
||||||
orderDate: '2024-01-20',
|
|
||||||
deliveryDate: '2024-01-23',
|
|
||||||
totalAmount: 425.50,
|
|
||||||
items: [
|
|
||||||
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
|
|
||||||
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
|
|
||||||
],
|
|
||||||
paymentStatus: 'paid',
|
|
||||||
notes: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'PO-2024-003',
|
|
||||||
supplier: 'Lácteos Frescos',
|
|
||||||
status: 'in_transit',
|
|
||||||
orderDate: '2024-01-24',
|
|
||||||
deliveryDate: '2024-01-26',
|
|
||||||
totalAmount: 320.75,
|
|
||||||
items: [
|
|
||||||
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
|
|
||||||
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
|
|
||||||
],
|
|
||||||
paymentStatus: 'pending',
|
|
||||||
notes: 'Producto refrigerado',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockSuppliers = [
|
if (!tenantId) {
|
||||||
{
|
return (
|
||||||
id: '1',
|
<div className="flex justify-center items-center h-64">
|
||||||
name: 'Molinos del Sur',
|
<div className="text-center">
|
||||||
contact: 'Juan Pérez',
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||||
email: 'juan@molinosdelsur.com',
|
No hay tenant seleccionado
|
||||||
phone: '+34 91 234 5678',
|
</h3>
|
||||||
category: 'Harinas',
|
<p className="text-[var(--text-secondary)]">
|
||||||
rating: 4.8,
|
Selecciona un tenant para ver los datos de procurement
|
||||||
totalOrders: 24,
|
</p>
|
||||||
totalSpent: 15600.00,
|
</div>
|
||||||
paymentTerms: '30 días',
|
</div>
|
||||||
leadTime: '2-3 días',
|
);
|
||||||
location: 'Sevilla',
|
}
|
||||||
status: 'active',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Levaduras SA',
|
|
||||||
contact: 'María González',
|
|
||||||
email: 'maria@levaduras.com',
|
|
||||||
phone: '+34 93 456 7890',
|
|
||||||
category: 'Levaduras',
|
|
||||||
rating: 4.6,
|
|
||||||
totalOrders: 18,
|
|
||||||
totalSpent: 8450.00,
|
|
||||||
paymentTerms: '15 días',
|
|
||||||
leadTime: '1-2 días',
|
|
||||||
location: 'Barcelona',
|
|
||||||
status: 'active',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Lácteos Frescos',
|
|
||||||
contact: 'Carlos Ruiz',
|
|
||||||
email: 'carlos@lacteosfrescos.com',
|
|
||||||
phone: '+34 96 789 0123',
|
|
||||||
category: 'Lácteos',
|
|
||||||
rating: 4.4,
|
|
||||||
totalOrders: 32,
|
|
||||||
totalSpent: 12300.00,
|
|
||||||
paymentTerms: '20 días',
|
|
||||||
leadTime: '1 día',
|
|
||||||
location: 'Valencia',
|
|
||||||
status: 'active',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getPurchaseStatusConfig = (status: string, paymentStatus: string) => {
|
|
||||||
|
const getPlanStatusConfig = (status: string) => {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
pending: { text: 'Pendiente', icon: Clock },
|
draft: { text: 'Borrador', icon: Clock },
|
||||||
|
pending_approval: { text: 'Pendiente Aprobación', icon: Clock },
|
||||||
approved: { text: 'Aprobado', icon: CheckCircle },
|
approved: { text: 'Aprobado', icon: CheckCircle },
|
||||||
in_transit: { text: 'En Tránsito', icon: Truck },
|
in_execution: { text: 'En Ejecución', icon: Truck },
|
||||||
delivered: { text: 'Entregado', icon: CheckCircle },
|
completed: { text: 'Completado', icon: CheckCircle },
|
||||||
cancelled: { text: 'Cancelado', icon: AlertCircle },
|
cancelled: { text: 'Cancelado', icon: AlertCircle },
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = statusConfig[status as keyof typeof statusConfig];
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
const Icon = config?.icon;
|
const Icon = config?.icon;
|
||||||
const isPaymentPending = paymentStatus === 'pending';
|
|
||||||
const isOverdue = paymentStatus === 'overdue';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
color: getStatusColor(status === 'in_transit' ? 'inTransit' : status),
|
color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status),
|
||||||
text: config?.text || status,
|
text: config?.text || status,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
isCritical: isOverdue,
|
isCritical: status === 'cancelled',
|
||||||
isHighlight: isPaymentPending
|
isHighlight: status === 'pending_approval'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredOrders = mockPurchaseOrders.filter(order => {
|
const filteredPlans = procurementPlans?.plans?.filter(plan => {
|
||||||
const matchesSearch = order.supplier.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch = plan.plan_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
plan.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
order.notes.toLowerCase().includes(searchTerm.toLowerCase());
|
(plan.special_requirements && plan.special_requirements.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
|
||||||
return matchesSearch;
|
return matchesSearch;
|
||||||
});
|
}) || [];
|
||||||
|
|
||||||
const mockPurchaseStats = {
|
const stats = {
|
||||||
totalOrders: mockPurchaseOrders.length,
|
totalPlans: dashboardData?.summary?.total_plans || 0,
|
||||||
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
|
activePlans: dashboardData?.summary?.active_plans || 0,
|
||||||
inTransit: mockPurchaseOrders.filter(o => o.status === 'in_transit').length,
|
pendingRequirements: dashboardData?.summary?.pending_requirements || 0,
|
||||||
delivered: mockPurchaseOrders.filter(o => o.status === 'delivered').length,
|
criticalRequirements: dashboardData?.summary?.critical_requirements || 0,
|
||||||
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
|
totalEstimatedCost: dashboardData?.summary?.total_estimated_cost || 0,
|
||||||
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
|
totalApprovedCost: dashboardData?.summary?.total_approved_cost || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const purchaseOrderStats = [
|
const procurementStats = [
|
||||||
{
|
{
|
||||||
title: 'Total Órdenes',
|
title: 'Planes Totales',
|
||||||
value: mockPurchaseStats.totalOrders,
|
value: stats.totalPlans,
|
||||||
variant: 'default' as const,
|
variant: 'default' as const,
|
||||||
icon: ShoppingCart,
|
icon: Package,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pendientes',
|
title: 'Planes Activos',
|
||||||
value: mockPurchaseStats.pendingOrders,
|
value: stats.activePlans,
|
||||||
variant: 'warning' as const,
|
|
||||||
icon: Clock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'En Tránsito',
|
|
||||||
value: mockPurchaseStats.inTransit,
|
|
||||||
variant: 'info' as const,
|
|
||||||
icon: Truck,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Entregadas',
|
|
||||||
value: mockPurchaseStats.delivered,
|
|
||||||
variant: 'success' as const,
|
variant: 'success' as const,
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Gasto Total',
|
title: 'Requerimientos Pendientes',
|
||||||
value: formatters.currency(mockPurchaseStats.totalSpent),
|
value: stats.pendingRequirements,
|
||||||
variant: 'success' as const,
|
variant: 'warning' as const,
|
||||||
|
icon: Clock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Críticos',
|
||||||
|
value: stats.criticalRequirements,
|
||||||
|
variant: 'warning' as const,
|
||||||
|
icon: AlertCircle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Costo Estimado',
|
||||||
|
value: formatters.currency(stats.totalEstimatedCost),
|
||||||
|
variant: 'info' as const,
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Proveedores',
|
title: 'Costo Aprobado',
|
||||||
value: mockPurchaseStats.activeSuppliers,
|
value: formatters.currency(stats.totalApprovedCost),
|
||||||
variant: 'info' as const,
|
variant: 'success' as const,
|
||||||
icon: Package,
|
icon: DollarSign,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Gestión de Compras"
|
title="Planificación de Compras"
|
||||||
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
|
description="Administra planes de compras, requerimientos y análisis de procurement"
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
id: "export",
|
id: "export",
|
||||||
label: "Exportar",
|
label: "Exportar",
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
icon: Download,
|
icon: Download,
|
||||||
onClick: () => console.log('Export purchase orders')
|
onClick: () => console.log('Export procurement data')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "new",
|
id: "generate",
|
||||||
label: "Nueva Orden de Compra",
|
label: "Generar Plan",
|
||||||
variant: "primary" as const,
|
variant: "primary" as const,
|
||||||
icon: Plus,
|
icon: Plus,
|
||||||
onClick: () => console.log('New purchase order')
|
onClick: () => generatePlanMutation.mutate({
|
||||||
|
tenantId,
|
||||||
|
request: {
|
||||||
|
force_regenerate: false,
|
||||||
|
planning_horizon_days: 14,
|
||||||
|
include_safety_stock: true,
|
||||||
|
safety_stock_percentage: 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trigger",
|
||||||
|
label: "Ejecutar Programador",
|
||||||
|
variant: "outline" as const,
|
||||||
|
icon: Calendar,
|
||||||
|
onClick: () => triggerSchedulerMutation.mutate(tenantId)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<StatsGrid
|
{isDashboardLoading ? (
|
||||||
stats={purchaseOrderStats}
|
<div className="flex justify-center items-center h-32">
|
||||||
columns={3}
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||||
/>
|
</div>
|
||||||
|
) : (
|
||||||
|
<StatsGrid
|
||||||
|
stats={procurementStats}
|
||||||
|
columns={3}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="border-b border-[var(--border-primary)]">
|
<div className="border-b border-[var(--border-primary)]">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('orders')}
|
onClick={() => setActiveTab('plans')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||||
activeTab === 'orders'
|
activeTab === 'plans'
|
||||||
? 'border-orange-500 text-[var(--color-primary)]'
|
? 'border-orange-500 text-[var(--color-primary)]'
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Órdenes de Compra
|
Planes de Compra
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('suppliers')}
|
onClick={() => setActiveTab('requirements')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||||
activeTab === 'suppliers'
|
activeTab === 'requirements'
|
||||||
? 'border-orange-500 text-[var(--color-primary)]'
|
? 'border-orange-500 text-[var(--color-primary)]'
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Proveedores
|
Requerimientos Críticos
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('analytics')}
|
onClick={() => setActiveTab('analytics')}
|
||||||
@@ -248,12 +218,12 @@ const ProcurementPage: React.FC = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'orders' && (
|
{activeTab === 'plans' && (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Buscar órdenes por proveedor, ID o notas..."
|
placeholder="Buscar planes por número, estado o notas..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -267,148 +237,151 @@ const ProcurementPage: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Purchase Orders Grid */}
|
{/* Procurement Plans Grid */}
|
||||||
{activeTab === 'orders' && (
|
{activeTab === 'plans' && (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{filteredOrders.map((order) => {
|
{isPlansLoading ? (
|
||||||
const statusConfig = getPurchaseStatusConfig(order.status, order.paymentStatus);
|
<div className="col-span-full flex justify-center items-center h-32">
|
||||||
const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : order.paymentStatus === 'overdue' ? 'Pago vencido' : '';
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
return (
|
) : (
|
||||||
<StatusCard
|
filteredPlans.map((plan) => {
|
||||||
key={order.id}
|
const statusConfig = getPlanStatusConfig(plan.status);
|
||||||
id={order.id}
|
|
||||||
statusIndicator={statusConfig}
|
return (
|
||||||
title={order.supplier}
|
<StatusCard
|
||||||
subtitle={order.id}
|
key={plan.id}
|
||||||
primaryValue={formatters.currency(order.totalAmount)}
|
id={plan.plan_number}
|
||||||
primaryValueLabel={`${order.items?.length} artículos`}
|
statusIndicator={statusConfig}
|
||||||
secondaryInfo={{
|
title={`Plan ${plan.plan_number}`}
|
||||||
label: 'Entrega',
|
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
|
||||||
value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} (pedido: ${new Date(order.orderDate).toLocaleDateString('es-ES')})`
|
primaryValue={formatters.currency(plan.total_estimated_cost)}
|
||||||
}}
|
primaryValueLabel={`${plan.total_requirements} requerimientos`}
|
||||||
metadata={[
|
secondaryInfo={{
|
||||||
...(order.notes ? [`"${order.notes}"`] : []),
|
label: 'Período',
|
||||||
...(paymentNote ? [paymentNote] : [])
|
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
|
||||||
]}
|
}}
|
||||||
actions={[
|
metadata={[
|
||||||
{
|
`${plan.planning_horizon_days} días de horizonte`,
|
||||||
label: 'Ver',
|
`Estrategia: ${plan.procurement_strategy}`,
|
||||||
icon: Eye,
|
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
|
||||||
variant: 'outline',
|
]}
|
||||||
onClick: () => {
|
actions={[
|
||||||
setSelectedOrder(order);
|
{
|
||||||
setModalMode('view');
|
label: 'Ver',
|
||||||
setShowForm(true);
|
icon: Eye,
|
||||||
}
|
variant: 'outline',
|
||||||
},
|
onClick: () => {
|
||||||
{
|
setSelectedPlan(plan);
|
||||||
label: 'Editar',
|
setModalMode('view');
|
||||||
icon: Edit,
|
setShowForm(true);
|
||||||
variant: 'outline',
|
}
|
||||||
onClick: () => {
|
},
|
||||||
setSelectedOrder(order);
|
...(plan.status === 'pending_approval' ? [{
|
||||||
setModalMode('edit');
|
label: 'Aprobar',
|
||||||
setShowForm(true);
|
icon: CheckCircle,
|
||||||
}
|
variant: 'outline' as const,
|
||||||
}
|
onClick: () => {
|
||||||
]}
|
updatePlanStatusMutation.mutate({
|
||||||
/>
|
tenant_id: tenantId,
|
||||||
);
|
plan_id: plan.id,
|
||||||
})}
|
status: 'approved'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}] : [])
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State for Purchase Orders */}
|
{/* Empty State for Procurement Plans */}
|
||||||
{activeTab === 'orders' && filteredOrders.length === 0 && (
|
{activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<ShoppingCart className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
<Package className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
||||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||||
No se encontraron órdenes de compra
|
No se encontraron planes de compra
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[var(--text-secondary)] mb-4">
|
<p className="text-[var(--text-secondary)] mb-4">
|
||||||
Intenta ajustar la búsqueda o crear una nueva orden de compra
|
Intenta ajustar la búsqueda o generar un nuevo plan de compra
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => console.log('New purchase order')}>
|
<Button
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
onClick={() => generatePlanMutation.mutate({
|
||||||
Nueva Orden de Compra
|
tenantId,
|
||||||
|
request: {
|
||||||
|
force_regenerate: false,
|
||||||
|
planning_horizon_days: 14,
|
||||||
|
include_safety_stock: true,
|
||||||
|
safety_stock_percentage: 20
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
disabled={generatePlanMutation.isPending}
|
||||||
|
>
|
||||||
|
{generatePlanMutation.isPending ? (
|
||||||
|
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Generar Plan de Compra
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'suppliers' && (
|
{activeTab === 'requirements' && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="space-y-4">
|
||||||
{mockSuppliers.map((supplier) => (
|
{isCriticalLoading ? (
|
||||||
<Card key={supplier.id} className="p-6">
|
<div className="flex justify-center items-center h-32">
|
||||||
<div className="flex items-start justify-between mb-4">
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||||
<div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{supplier.name}</h3>
|
) : criticalRequirements && criticalRequirements.length > 0 ? (
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{supplier.category}</p>
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
</div>
|
{criticalRequirements.map((requirement) => (
|
||||||
<Badge variant="green">Activo</Badge>
|
<StatusCard
|
||||||
</div>
|
key={requirement.id}
|
||||||
|
id={requirement.requirement_number}
|
||||||
<div className="space-y-2 mb-4">
|
statusIndicator={{
|
||||||
<div className="flex justify-between text-sm">
|
color: getStatusColor('danger'),
|
||||||
<span className="text-[var(--text-secondary)]">Contacto:</span>
|
text: 'Crítico',
|
||||||
<span className="font-medium">{supplier.contact}</span>
|
icon: AlertCircle,
|
||||||
</div>
|
isCritical: true
|
||||||
<div className="flex justify-between text-sm">
|
}}
|
||||||
<span className="text-[var(--text-secondary)]">Email:</span>
|
title={requirement.product_name}
|
||||||
<span className="font-medium">{supplier.email}</span>
|
subtitle={requirement.requirement_number}
|
||||||
</div>
|
primaryValue={`${requirement.required_quantity} ${requirement.unit_of_measure}`}
|
||||||
<div className="flex justify-between text-sm">
|
primaryValueLabel="Cantidad requerida"
|
||||||
<span className="text-[var(--text-secondary)]">Teléfono:</span>
|
secondaryInfo={{
|
||||||
<span className="font-medium">{supplier.phone}</span>
|
label: 'Fecha límite',
|
||||||
</div>
|
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES')
|
||||||
<div className="flex justify-between text-sm">
|
}}
|
||||||
<span className="text-[var(--text-secondary)]">Ubicación:</span>
|
metadata={[
|
||||||
<span className="font-medium">{supplier.location}</span>
|
`Stock actual: ${requirement.current_stock_level} ${requirement.unit_of_measure}`,
|
||||||
</div>
|
`Proveedor: ${requirement.supplier_name || 'No asignado'}`,
|
||||||
</div>
|
`Costo estimado: ${formatters.currency(requirement.estimated_total_cost || 0)}`
|
||||||
|
]}
|
||||||
<div className="border-t pt-4">
|
actions={[
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
{
|
||||||
<div>
|
label: 'Ver Detalles',
|
||||||
<p className="text-xs text-[var(--text-secondary)]">Valoración</p>
|
icon: Eye,
|
||||||
<p className="text-sm font-medium flex items-center">
|
variant: 'outline',
|
||||||
<span className="text-yellow-500">★</span>
|
onClick: () => console.log('View requirement details')
|
||||||
<span className="ml-1">{supplier.rating}</span>
|
}
|
||||||
</p>
|
]}
|
||||||
</div>
|
/>
|
||||||
<div>
|
))}
|
||||||
<p className="text-xs text-[var(--text-secondary)]">Pedidos</p>
|
</div>
|
||||||
<p className="text-sm font-medium">{supplier.totalOrders}</p>
|
) : (
|
||||||
</div>
|
<div className="text-center py-12">
|
||||||
</div>
|
<CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
No hay requerimientos críticos
|
||||||
<div>
|
</h3>
|
||||||
<p className="text-xs text-[var(--text-secondary)]">Total Gastado</p>
|
<p className="text-[var(--text-secondary)]">
|
||||||
<p className="text-sm font-medium">€{supplier.totalSpent.toLocaleString()}</p>
|
Todos los requerimientos están bajo control
|
||||||
</div>
|
</p>
|
||||||
<div>
|
</div>
|
||||||
<p className="text-xs text-[var(--text-secondary)]">Tiempo Entrega</p>
|
)}
|
||||||
<p className="text-sm font-medium">{supplier.leadTime}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-xs text-[var(--text-secondary)]">Condiciones de Pago</p>
|
|
||||||
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline" size="sm" className="flex-1">
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" className="flex-1">
|
|
||||||
Nuevo Pedido
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -416,100 +389,139 @@ const ProcurementPage: React.FC = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Mes</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Costos de Procurement</h3>
|
||||||
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
|
<div className="space-y-4">
|
||||||
<p className="text-[var(--text-tertiary)]">Gráfico de gastos mensuales</p>
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">Costo Estimado Total</span>
|
||||||
|
<span className="text-lg font-semibold text-[var(--text-primary)]">
|
||||||
|
{formatters.currency(stats.totalEstimatedCost)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">Costo Aprobado</span>
|
||||||
|
<span className="text-lg font-semibold text-green-600">
|
||||||
|
{formatters.currency(stats.totalApprovedCost)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">Varianza</span>
|
||||||
|
<span className="text-lg font-semibold text-[var(--text-primary)]">
|
||||||
|
{formatters.currency(stats.totalEstimatedCost - stats.totalApprovedCost)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Top Proveedores</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Críticas</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{mockSuppliers
|
{dashboardData?.low_stock_alerts?.slice(0, 5).map((alert: any, index: number) => (
|
||||||
.sort((a, b) => b.totalSpent - a.totalSpent)
|
<div key={index} className="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
||||||
.slice(0, 5)
|
<div className="flex items-center">
|
||||||
.map((supplier, index) => (
|
<AlertCircle className="w-4 h-4 text-red-500 mr-2" />
|
||||||
<div key={supplier.id} className="flex items-center justify-between">
|
<span className="text-sm text-[var(--text-primary)]">{alert.product_name || `Alerta ${index + 1}`}</span>
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-tertiary)] w-4">
|
|
||||||
{index + 1}.
|
|
||||||
</span>
|
|
||||||
<span className="ml-3 text-sm text-[var(--text-primary)]">{supplier.name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
€{supplier.totalSpent.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<span className="text-xs text-red-600 font-medium">
|
||||||
|
Stock Bajo
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)) || (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<CheckCircle className="mx-auto h-8 w-8 text-green-500 mb-2" />
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">No hay alertas críticas</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Categoría</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen de Performance</h3>
|
||||||
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<p className="text-[var(--text-tertiary)]">Gráfico de gastos por categoría</p>
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.totalPlans}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Totales</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-green-600">{stats.activePlans}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Activos</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-yellow-600">{stats.pendingRequirements}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mt-1">Pendientes</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-red-600">{stats.criticalRequirements}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mt-1">Críticos</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Purchase Order Modal */}
|
{/* Procurement Plan Modal */}
|
||||||
{showForm && selectedOrder && (
|
{showForm && selectedPlan && (
|
||||||
<StatusModal
|
<StatusModal
|
||||||
isOpen={showForm}
|
isOpen={showForm}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setSelectedOrder(null);
|
setSelectedPlan(null);
|
||||||
setModalMode('view');
|
setModalMode('view');
|
||||||
}}
|
}}
|
||||||
mode={modalMode}
|
mode={modalMode}
|
||||||
onModeChange={setModalMode}
|
onModeChange={setModalMode}
|
||||||
title={selectedOrder.supplier}
|
title={`Plan de Compra ${selectedPlan.plan_number}`}
|
||||||
subtitle={`Orden de Compra ${selectedOrder.id}`}
|
subtitle={new Date(selectedPlan.plan_date).toLocaleDateString('es-ES')}
|
||||||
statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
|
statusIndicator={getPlanStatusConfig(selectedPlan.status)}
|
||||||
size="lg"
|
size="lg"
|
||||||
sections={[
|
sections={[
|
||||||
{
|
{
|
||||||
title: 'Información del Proveedor',
|
title: 'Información del Plan',
|
||||||
icon: Package,
|
icon: Package,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Proveedor',
|
label: 'Número de Plan',
|
||||||
value: selectedOrder.supplier,
|
value: selectedPlan.plan_number,
|
||||||
highlight: true,
|
highlight: true
|
||||||
editable: true,
|
|
||||||
required: true,
|
|
||||||
placeholder: 'Nombre del proveedor'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ID de Orden',
|
label: 'Tipo de Plan',
|
||||||
value: selectedOrder.id
|
value: selectedPlan.plan_type
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Estado de Pago',
|
label: 'Estrategia',
|
||||||
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido',
|
value: selectedPlan.procurement_strategy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Prioridad',
|
||||||
|
value: selectedPlan.priority,
|
||||||
type: 'status'
|
type: 'status'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Fechas Importantes',
|
title: 'Fechas y Períodos',
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Fecha de pedido',
|
label: 'Fecha del Plan',
|
||||||
value: selectedOrder.orderDate,
|
value: selectedPlan.plan_date,
|
||||||
type: 'date',
|
type: 'date'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fecha de entrega',
|
label: 'Período de Inicio',
|
||||||
value: selectedOrder.deliveryDate,
|
value: selectedPlan.plan_period_start,
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Período de Fin',
|
||||||
|
value: selectedPlan.plan_period_end,
|
||||||
type: 'date',
|
type: 'date',
|
||||||
highlight: true,
|
highlight: true
|
||||||
editable: true,
|
},
|
||||||
required: true
|
{
|
||||||
|
label: 'Horizonte de Planificación',
|
||||||
|
value: `${selectedPlan.planning_horizon_days} días`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -518,47 +530,60 @@ const ProcurementPage: React.FC = () => {
|
|||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Importe total',
|
label: 'Costo Estimado Total',
|
||||||
value: selectedOrder.totalAmount,
|
value: selectedPlan.total_estimated_cost,
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
highlight: true,
|
highlight: true
|
||||||
editable: true,
|
|
||||||
required: true,
|
|
||||||
placeholder: '0.00'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Número de artículos',
|
label: 'Costo Aprobado Total',
|
||||||
value: `${selectedOrder.items?.length} productos`
|
value: selectedPlan.total_approved_cost,
|
||||||
|
type: 'currency'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Varianza de Costo',
|
||||||
|
value: selectedPlan.cost_variance,
|
||||||
|
type: 'currency'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Artículos Pedidos',
|
title: 'Estadísticas',
|
||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Lista de productos',
|
label: 'Total de Requerimientos',
|
||||||
value: selectedOrder.items?.map(item => `${item.name}: ${item.quantity} ${item.unit} - ${formatters.currency(item.total)}`),
|
value: `${selectedPlan.total_requirements} requerimientos`
|
||||||
type: 'list',
|
},
|
||||||
span: 2
|
{
|
||||||
|
label: 'Demanda Total (Cantidad)',
|
||||||
|
value: `${selectedPlan.total_demand_quantity} unidades`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Proveedores Primarios',
|
||||||
|
value: `${selectedPlan.primary_suppliers_count} proveedores`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Proveedores de Respaldo',
|
||||||
|
value: `${selectedPlan.backup_suppliers_count} proveedores`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...(selectedOrder.notes ? [{
|
...(selectedPlan.special_requirements ? [{
|
||||||
title: 'Notas Adicionales',
|
title: 'Requerimientos Especiales',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Observaciones',
|
label: 'Observaciones',
|
||||||
value: selectedOrder.notes,
|
value: selectedPlan.special_requirements,
|
||||||
span: 2 as const,
|
span: 2 as const,
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholder: 'Añadir notas sobre la orden de compra...'
|
placeholder: 'Añadir requerimientos especiales para el plan...'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}] : [])
|
}] : [])
|
||||||
]}
|
]}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
console.log('Editing purchase order:', selectedOrder.id);
|
console.log('Editing procurement plan:', selectedPlan.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
|||||||
app.include_router(notification.router, prefix="/api/v1/notifications", tags=["notifications"])
|
app.include_router(notification.router, prefix="/api/v1/notifications", tags=["notifications"])
|
||||||
app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location"])
|
app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location"])
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
"""Application startup"""
|
"""Application startup"""
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ async def proxy_tenant_notifications(request: Request, tenant_id: str = Path(...
|
|||||||
# TENANT-SCOPED INVENTORY SERVICE ENDPOINTS
|
# TENANT-SCOPED INVENTORY SERVICE ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
|
@router.api_route("/{tenant_id}/alerts{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||||
|
async def proxy_tenant_alerts(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||||
|
"""Proxy tenant alerts requests to inventory service"""
|
||||||
|
target_path = f"/api/v1/tenants/{tenant_id}/alerts{path}".rstrip("/")
|
||||||
|
return await _proxy_to_inventory_service(request, target_path, tenant_id=tenant_id)
|
||||||
|
|
||||||
@router.api_route("/{tenant_id}/inventory/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
@router.api_route("/{tenant_id}/inventory/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||||
async def proxy_tenant_inventory(request: Request, tenant_id: str = Path(...), path: str = ""):
|
async def proxy_tenant_inventory(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||||
"""Proxy tenant inventory requests to inventory service"""
|
"""Proxy tenant inventory requests to inventory service"""
|
||||||
@@ -179,6 +185,18 @@ async def proxy_tenant_orders(request: Request, tenant_id: str = Path(...), path
|
|||||||
target_path = f"/api/v1/tenants/{tenant_id}/orders/{path}".rstrip("/")
|
target_path = f"/api/v1/tenants/{tenant_id}/orders/{path}".rstrip("/")
|
||||||
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id)
|
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id)
|
||||||
|
|
||||||
|
@router.api_route("/{tenant_id}/customers/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||||
|
async def proxy_tenant_customers(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||||
|
"""Proxy tenant customers requests to orders service"""
|
||||||
|
target_path = f"/api/v1/tenants/{tenant_id}/customers/{path}".rstrip("/")
|
||||||
|
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id)
|
||||||
|
|
||||||
|
@router.api_route("/{tenant_id}/procurement/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||||
|
async def proxy_tenant_procurement(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||||
|
"""Proxy tenant procurement requests to orders service"""
|
||||||
|
target_path = f"/api/v1/tenants/{tenant_id}/procurement/{path}".rstrip("/")
|
||||||
|
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id)
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# TENANT-SCOPED SUPPLIER SERVICE ENDPOINTS
|
# TENANT-SCOPED SUPPLIER SERVICE ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ from shared.clients.forecast_client import ForecastServiceClient
|
|||||||
from shared.config.base import BaseServiceSettings
|
from shared.config.base import BaseServiceSettings
|
||||||
from shared.monitoring.decorators import monitor_performance
|
from shared.monitoring.decorators import monitor_performance
|
||||||
|
|
||||||
# Create router
|
# Create router - tenant-scoped
|
||||||
router = APIRouter(prefix="/procurement-plans", tags=["Procurement Planning"])
|
router = APIRouter(prefix="/tenants/{tenant_id}", tags=["Procurement Planning"])
|
||||||
|
|
||||||
# Create service settings
|
# Create service settings
|
||||||
service_settings = BaseServiceSettings()
|
service_settings = BaseServiceSettings()
|
||||||
@@ -82,9 +82,10 @@ async def get_procurement_service(db: AsyncSession = Depends(get_db)) -> Procure
|
|||||||
# PROCUREMENT PLAN ENDPOINTS
|
# PROCUREMENT PLAN ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@router.get("/current", response_model=Optional[ProcurementPlanResponse])
|
@router.get("/procurement/plans/current", response_model=Optional[ProcurementPlanResponse])
|
||||||
@monitor_performance("get_current_procurement_plan")
|
@monitor_performance("get_current_procurement_plan")
|
||||||
async def get_current_procurement_plan(
|
async def get_current_procurement_plan(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
):
|
):
|
||||||
@@ -103,9 +104,10 @@ async def get_current_procurement_plan(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{plan_date}", response_model=Optional[ProcurementPlanResponse])
|
@router.get("/procurement/plans/date/{plan_date}", response_model=Optional[ProcurementPlanResponse])
|
||||||
@monitor_performance("get_procurement_plan_by_date")
|
@monitor_performance("get_procurement_plan_by_date")
|
||||||
async def get_procurement_plan_by_date(
|
async def get_procurement_plan_by_date(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
plan_date: date,
|
plan_date: date,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
@@ -125,9 +127,10 @@ async def get_procurement_plan_by_date(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=PaginatedProcurementPlans)
|
@router.get("/procurement/plans", response_model=PaginatedProcurementPlans)
|
||||||
@monitor_performance("list_procurement_plans")
|
@monitor_performance("list_procurement_plans")
|
||||||
async def list_procurement_plans(
|
async def list_procurement_plans(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
status: Optional[str] = Query(None, description="Filter by plan status"),
|
status: Optional[str] = Query(None, description="Filter by plan status"),
|
||||||
start_date: Optional[date] = Query(None, description="Start date filter (YYYY-MM-DD)"),
|
start_date: Optional[date] = Query(None, description="Start date filter (YYYY-MM-DD)"),
|
||||||
end_date: Optional[date] = Query(None, description="End date filter (YYYY-MM-DD)"),
|
end_date: Optional[date] = Query(None, description="End date filter (YYYY-MM-DD)"),
|
||||||
@@ -175,9 +178,10 @@ async def list_procurement_plans(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate", response_model=GeneratePlanResponse)
|
@router.post("/procurement/plans/generate", response_model=GeneratePlanResponse)
|
||||||
@monitor_performance("generate_procurement_plan")
|
@monitor_performance("generate_procurement_plan")
|
||||||
async def generate_procurement_plan(
|
async def generate_procurement_plan(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
request: GeneratePlanRequest,
|
request: GeneratePlanRequest,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
@@ -216,9 +220,10 @@ async def generate_procurement_plan(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{plan_id}/status")
|
@router.put("/procurement/plans/{plan_id}/status")
|
||||||
@monitor_performance("update_procurement_plan_status")
|
@monitor_performance("update_procurement_plan_status")
|
||||||
async def update_procurement_plan_status(
|
async def update_procurement_plan_status(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
plan_id: uuid.UUID,
|
plan_id: uuid.UUID,
|
||||||
status: str = Query(..., description="New status", pattern="^(draft|pending_approval|approved|in_execution|completed|cancelled)$"),
|
status: str = Query(..., description="New status", pattern="^(draft|pending_approval|approved|in_execution|completed|cancelled)$"),
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
@@ -254,9 +259,10 @@ async def update_procurement_plan_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{plan_id}", response_model=Optional[ProcurementPlanResponse])
|
@router.get("/procurement/plans/id/{plan_id}", response_model=Optional[ProcurementPlanResponse])
|
||||||
@monitor_performance("get_procurement_plan_by_id")
|
@monitor_performance("get_procurement_plan_by_id")
|
||||||
async def get_procurement_plan_by_id(
|
async def get_procurement_plan_by_id(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
plan_id: uuid.UUID,
|
plan_id: uuid.UUID,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
@@ -290,9 +296,10 @@ async def get_procurement_plan_by_id(
|
|||||||
# DASHBOARD ENDPOINTS
|
# DASHBOARD ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@router.get("/dashboard/data", response_model=Optional[DashboardData])
|
@router.get("/procurement/dashboard", response_model=Optional[DashboardData])
|
||||||
@monitor_performance("get_procurement_dashboard")
|
@monitor_performance("get_procurement_dashboard")
|
||||||
async def get_procurement_dashboard(
|
async def get_procurement_dashboard(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
):
|
):
|
||||||
@@ -321,9 +328,10 @@ async def get_procurement_dashboard(
|
|||||||
# REQUIREMENT MANAGEMENT ENDPOINTS
|
# REQUIREMENT MANAGEMENT ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@router.get("/{plan_id}/requirements")
|
@router.get("/procurement/plans/{plan_id}/requirements")
|
||||||
@monitor_performance("get_plan_requirements")
|
@monitor_performance("get_plan_requirements")
|
||||||
async def get_plan_requirements(
|
async def get_plan_requirements(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
plan_id: uuid.UUID,
|
plan_id: uuid.UUID,
|
||||||
status: Optional[str] = Query(None, description="Filter by requirement status"),
|
status: Optional[str] = Query(None, description="Filter by requirement status"),
|
||||||
priority: Optional[str] = Query(None, description="Filter by priority level"),
|
priority: Optional[str] = Query(None, description="Filter by priority level"),
|
||||||
@@ -364,9 +372,10 @@ async def get_plan_requirements(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/requirements/critical")
|
@router.get("/procurement/requirements/critical")
|
||||||
@monitor_performance("get_critical_requirements")
|
@monitor_performance("get_critical_requirements")
|
||||||
async def get_critical_requirements(
|
async def get_critical_requirements(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
):
|
):
|
||||||
@@ -391,9 +400,10 @@ async def get_critical_requirements(
|
|||||||
# UTILITY ENDPOINTS
|
# UTILITY ENDPOINTS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@router.post("/scheduler/trigger")
|
@router.post("/procurement/scheduler/trigger")
|
||||||
@monitor_performance("trigger_daily_scheduler")
|
@monitor_performance("trigger_daily_scheduler")
|
||||||
async def trigger_daily_scheduler(
|
async def trigger_daily_scheduler(
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||||
):
|
):
|
||||||
@@ -419,7 +429,7 @@ async def trigger_daily_scheduler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
@router.get("/procurement/health")
|
||||||
async def procurement_health_check():
|
async def procurement_health_check():
|
||||||
"""
|
"""
|
||||||
Health check endpoint for procurement service
|
Health check endpoint for procurement service
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class ProcurementPlanRepository(BaseRepository):
|
|||||||
"""Repository for procurement plan operations"""
|
"""Repository for procurement plan operations"""
|
||||||
|
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
super().__init__(db, ProcurementPlan)
|
super().__init__(ProcurementPlan)
|
||||||
|
self.db = db
|
||||||
|
|
||||||
async def create_plan(self, plan_data: Dict[str, Any]) -> ProcurementPlan:
|
async def create_plan(self, plan_data: Dict[str, Any]) -> ProcurementPlan:
|
||||||
"""Create a new procurement plan"""
|
"""Create a new procurement plan"""
|
||||||
@@ -134,7 +135,8 @@ class ProcurementRequirementRepository(BaseRepository):
|
|||||||
"""Repository for procurement requirement operations"""
|
"""Repository for procurement requirement operations"""
|
||||||
|
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
super().__init__(db, ProcurementRequirement)
|
super().__init__(ProcurementRequirement)
|
||||||
|
self.db = db
|
||||||
|
|
||||||
async def create_requirement(self, requirement_data: Dict[str, Any]) -> ProcurementRequirement:
|
async def create_requirement(self, requirement_data: Dict[str, Any]) -> ProcurementRequirement:
|
||||||
"""Create a new procurement requirement"""
|
"""Create a new procurement requirement"""
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class CacheService:
|
|||||||
self.redis_url,
|
self.redis_url,
|
||||||
decode_responses=True,
|
decode_responses=True,
|
||||||
socket_keepalive=True,
|
socket_keepalive=True,
|
||||||
socket_keepalive_options={"TCP_KEEPIDLE": 1, "TCP_KEEPINTVL": 3, "TCP_KEEPCNT": 5},
|
socket_keepalive_options={1: 1, 3: 3, 5: 5}, # Use integer keys
|
||||||
retry_on_timeout=True,
|
retry_on_timeout=True,
|
||||||
max_connections=50
|
max_connections=50
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user