Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

@@ -0,0 +1,87 @@
/**
* Auth Service - Mirror backend auth endpoints
*/
import { apiClient } from '../client';
import {
UserRegistration,
UserLogin,
TokenResponse,
RefreshTokenRequest,
PasswordChange,
PasswordReset,
UserResponse,
UserUpdate,
TokenVerificationResponse,
AuthHealthResponse,
} from '../types/auth';
export class AuthService {
private readonly baseUrl = '/auth';
async register(userData: UserRegistration): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/register`, userData);
}
async login(loginData: UserLogin): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/login`, loginData);
}
async refreshToken(refreshToken: string): Promise<TokenResponse> {
const refreshData: RefreshTokenRequest = { refresh_token: refreshToken };
return apiClient.post<TokenResponse>(`${this.baseUrl}/refresh`, refreshData);
}
async verifyToken(token?: string): Promise<TokenVerificationResponse> {
// If token is provided, temporarily set it; otherwise use current token
const currentToken = apiClient.getAuthToken();
if (token && token !== currentToken) {
apiClient.setAuthToken(token);
}
const response = await apiClient.post<TokenVerificationResponse>(`${this.baseUrl}/verify`);
// Restore original token if we temporarily changed it
if (token && token !== currentToken) {
apiClient.setAuthToken(currentToken);
}
return response;
}
async logout(refreshToken: string): Promise<{ message: string }> {
const refreshData: RefreshTokenRequest = { refresh_token: refreshToken };
return apiClient.post<{ message: string }>(`${this.baseUrl}/logout`, refreshData);
}
async changePassword(passwordData: PasswordChange): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/change-password`, passwordData);
}
async resetPassword(resetData: PasswordReset): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/reset-password`, resetData);
}
async getProfile(): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/profile`);
}
async updateProfile(updateData: UserUpdate): Promise<UserResponse> {
return apiClient.put<UserResponse>(`${this.baseUrl}/profile`, updateData);
}
async verifyEmail(
userId: string,
verificationToken: string
): Promise<{ message: string }> {
return apiClient.post<{ message: string }>(`${this.baseUrl}/verify-email`, {
user_id: userId,
verification_token: verificationToken,
});
}
async healthCheck(): Promise<AuthHealthResponse> {
return apiClient.get<AuthHealthResponse>(`${this.baseUrl}/health`);
}
}
export const authService = new AuthService();

View File

@@ -0,0 +1,94 @@
/**
* Classification Service - Mirror backend classification endpoints
*/
import { apiClient } from '../client';
import {
ProductClassificationRequest,
BatchClassificationRequest,
ProductSuggestionResponse,
BusinessModelAnalysisResponse,
ClassificationApprovalRequest,
ClassificationApprovalResponse,
} from '../types/classification';
export class ClassificationService {
private readonly baseUrl = '/tenants';
async classifyProduct(
tenantId: string,
classificationData: ProductClassificationRequest
): Promise<ProductSuggestionResponse> {
return apiClient.post<ProductSuggestionResponse>(
`${this.baseUrl}/${tenantId}/classification/classify-product`,
classificationData
);
}
async classifyProductsBatch(
tenantId: string,
batchData: BatchClassificationRequest
): Promise<ProductSuggestionResponse[]> {
return apiClient.post<ProductSuggestionResponse[]>(
`${this.baseUrl}/${tenantId}/classification/classify-batch`,
batchData
);
}
async getBusinessModelAnalysis(tenantId: string): Promise<BusinessModelAnalysisResponse> {
return apiClient.get<BusinessModelAnalysisResponse>(
`${this.baseUrl}/${tenantId}/classification/business-model-analysis`
);
}
async approveClassification(
tenantId: string,
approvalData: ClassificationApprovalRequest
): Promise<ClassificationApprovalResponse> {
return apiClient.post<ClassificationApprovalResponse>(
`${this.baseUrl}/${tenantId}/classification/approve-suggestion`,
approvalData
);
}
async getPendingSuggestions(tenantId: string): Promise<ProductSuggestionResponse[]> {
return apiClient.get<ProductSuggestionResponse[]>(
`${this.baseUrl}/${tenantId}/classification/pending-suggestions`
);
}
async getSuggestionHistory(
tenantId: string,
limit: number = 50,
offset: number = 0
): Promise<{
items: ProductSuggestionResponse[];
total: number;
}> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/classification/suggestion-history?${queryParams.toString()}`
);
}
async deleteSuggestion(tenantId: string, suggestionId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`
);
}
async updateSuggestion(
tenantId: string,
suggestionId: string,
updateData: Partial<ProductSuggestionResponse>
): Promise<ProductSuggestionResponse> {
return apiClient.put<ProductSuggestionResponse>(
`${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`,
updateData
);
}
}
export const classificationService = new ClassificationService();

View File

@@ -0,0 +1,99 @@
/**
* Data Import Service - Mirror backend data import endpoints
*/
import { apiClient } from '../client';
import {
ImportValidationRequest,
ImportValidationResponse,
ImportProcessRequest,
ImportProcessResponse,
ImportStatusResponse
} from '../types/dataImport';
export class DataImportService {
private readonly baseUrl = '/tenants';
async validateJsonData(
tenantId: string,
data: any
): Promise<ImportValidationResponse> {
return apiClient.post<ImportValidationResponse>(
`${this.baseUrl}/${tenantId}/sales/import/validate-json`,
data
);
}
async validateCsvFile(
tenantId: string,
file: File
): Promise<ImportValidationResponse> {
return apiClient.uploadFile<ImportValidationResponse>(
`${this.baseUrl}/${tenantId}/sales/import/validate-csv`,
file
);
}
async importJsonData(
tenantId: string,
data: any,
options?: {
skip_validation?: boolean;
chunk_size?: number;
}
): Promise<ImportProcessResponse> {
const payload = {
...data,
options,
};
return apiClient.post<ImportProcessResponse>(
`${this.baseUrl}/${tenantId}/sales/import/json`,
payload
);
}
async importCsvFile(
tenantId: string,
file: File,
options?: {
skip_validation?: boolean;
chunk_size?: number;
}
): Promise<ImportProcessResponse> {
const formData = new FormData();
formData.append('file', file);
if (options) {
formData.append('options', JSON.stringify(options));
}
return apiClient.uploadFile<ImportProcessResponse>(
`${this.baseUrl}/${tenantId}/sales/import/csv`,
formData
);
}
async getImportStatus(
tenantId: string,
importId: string
): Promise<ImportStatusResponse> {
return apiClient.get<ImportStatusResponse>(
`${this.baseUrl}/${tenantId}/sales/import/${importId}/status`
);
}
async cancelImport(
tenantId: string,
importId: string
): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(
`${this.baseUrl}/${tenantId}/sales/import/${importId}/cancel`
);
}
async getImportHistory(tenantId: string): Promise<ImportStatusResponse[]> {
return apiClient.get<ImportStatusResponse[]>(
`${this.baseUrl}/${tenantId}/sales/import/history`
);
}
}
export const dataImportService = new DataImportService();

View File

@@ -0,0 +1,273 @@
/**
* Food Safety Service - Mirror backend food safety endpoints
*/
import { apiClient } from '../client';
import {
FoodSafetyComplianceCreate,
FoodSafetyComplianceUpdate,
FoodSafetyComplianceResponse,
TemperatureLogCreate,
BulkTemperatureLogCreate,
TemperatureLogResponse,
FoodSafetyAlertCreate,
FoodSafetyAlertUpdate,
FoodSafetyAlertResponse,
FoodSafetyFilter,
TemperatureMonitoringFilter,
FoodSafetyMetrics,
TemperatureAnalytics,
FoodSafetyDashboard,
} from '../types/foodSafety';
import { PaginatedResponse } from '../types/inventory';
export class FoodSafetyService {
private readonly baseUrl = '/tenants';
// Compliance Management
async createComplianceRecord(
tenantId: string,
complianceData: FoodSafetyComplianceCreate
): Promise<FoodSafetyComplianceResponse> {
return apiClient.post<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance`,
complianceData
);
}
async getComplianceRecord(
tenantId: string,
recordId: string
): Promise<FoodSafetyComplianceResponse> {
return apiClient.get<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`
);
}
async getComplianceRecords(
tenantId: string,
filter?: FoodSafetyFilter
): Promise<PaginatedResponse<FoodSafetyComplianceResponse>> {
const queryParams = new URLSearchParams();
if (filter?.compliance_type) queryParams.append('compliance_type', filter.compliance_type);
if (filter?.status) queryParams.append('status', filter.status);
if (filter?.ingredient_id) queryParams.append('ingredient_id', filter.ingredient_id);
if (filter?.resolved !== undefined) queryParams.append('resolved', filter.resolved.toString());
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/compliance?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/compliance`;
return apiClient.get<PaginatedResponse<FoodSafetyComplianceResponse>>(url);
}
async updateComplianceRecord(
tenantId: string,
recordId: string,
updateData: FoodSafetyComplianceUpdate
): Promise<FoodSafetyComplianceResponse> {
return apiClient.put<FoodSafetyComplianceResponse>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`,
updateData
);
}
async deleteComplianceRecord(
tenantId: string,
recordId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/food-safety/compliance/${recordId}`
);
}
// Temperature Monitoring
async createTemperatureLog(
tenantId: string,
logData: TemperatureLogCreate
): Promise<TemperatureLogResponse> {
return apiClient.post<TemperatureLogResponse>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-logs`,
logData
);
}
async createBulkTemperatureLogs(
tenantId: string,
bulkData: BulkTemperatureLogCreate
): Promise<{
created_count: number;
failed_count: number;
errors?: string[];
}> {
return apiClient.post(
`${this.baseUrl}/${tenantId}/food-safety/temperature-logs/bulk`,
bulkData
);
}
async getTemperatureLogs(
tenantId: string,
filter?: TemperatureMonitoringFilter
): Promise<PaginatedResponse<TemperatureLogResponse>> {
const queryParams = new URLSearchParams();
if (filter?.location) queryParams.append('location', filter.location);
if (filter?.equipment_id) queryParams.append('equipment_id', filter.equipment_id);
if (filter?.temperature_range?.min !== undefined)
queryParams.append('min_temperature', filter.temperature_range.min.toString());
if (filter?.temperature_range?.max !== undefined)
queryParams.append('max_temperature', filter.temperature_range.max.toString());
if (filter?.alert_triggered !== undefined)
queryParams.append('alert_triggered', filter.alert_triggered.toString());
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/temperature-logs?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/temperature-logs`;
return apiClient.get<PaginatedResponse<TemperatureLogResponse>>(url);
}
async getTemperatureAnalytics(
tenantId: string,
location: string,
startDate?: string,
endDate?: string
): Promise<TemperatureAnalytics> {
const queryParams = new URLSearchParams();
queryParams.append('location', location);
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
return apiClient.get<TemperatureAnalytics>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-analytics?${queryParams.toString()}`
);
}
// Alert Management
async createFoodSafetyAlert(
tenantId: string,
alertData: FoodSafetyAlertCreate
): Promise<FoodSafetyAlertResponse> {
return apiClient.post<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts`,
alertData
);
}
async getFoodSafetyAlert(
tenantId: string,
alertId: string
): Promise<FoodSafetyAlertResponse> {
return apiClient.get<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`
);
}
async getFoodSafetyAlerts(
tenantId: string,
status?: 'open' | 'in_progress' | 'resolved' | 'dismissed',
severity?: 'critical' | 'warning' | 'info',
limit: number = 50,
offset: number = 0
): Promise<PaginatedResponse<FoodSafetyAlertResponse>> {
const queryParams = new URLSearchParams();
if (status) queryParams.append('status', status);
if (severity) queryParams.append('severity', severity);
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get<PaginatedResponse<FoodSafetyAlertResponse>>(
`${this.baseUrl}/${tenantId}/food-safety/alerts?${queryParams.toString()}`
);
}
async updateFoodSafetyAlert(
tenantId: string,
alertId: string,
updateData: FoodSafetyAlertUpdate
): Promise<FoodSafetyAlertResponse> {
return apiClient.put<FoodSafetyAlertResponse>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`,
updateData
);
}
async deleteFoodSafetyAlert(
tenantId: string,
alertId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(
`${this.baseUrl}/${tenantId}/food-safety/alerts/${alertId}`
);
}
// Dashboard and Metrics
async getFoodSafetyDashboard(tenantId: string): Promise<FoodSafetyDashboard> {
return apiClient.get<FoodSafetyDashboard>(
`${this.baseUrl}/${tenantId}/food-safety/dashboard`
);
}
async getFoodSafetyMetrics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<FoodSafetyMetrics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/metrics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/metrics`;
return apiClient.get<FoodSafetyMetrics>(url);
}
async getTemperatureViolations(
tenantId: string,
limit: number = 20
): Promise<TemperatureLogResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get<TemperatureLogResponse[]>(
`${this.baseUrl}/${tenantId}/food-safety/temperature-violations?${queryParams.toString()}`
);
}
async getComplianceRate(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
overall_rate: number;
by_type: Record<string, number>;
trend: Array<{ date: string; rate: number }>;
}> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/food-safety/compliance-rate?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/food-safety/compliance-rate`;
return apiClient.get(url);
}
}
export const foodSafetyService = new FoodSafetyService();

View File

@@ -0,0 +1,228 @@
/**
* Inventory Service - Mirror backend inventory endpoints
*/
import { apiClient } from '../client';
import {
IngredientCreate,
IngredientUpdate,
IngredientResponse,
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
InventoryFilter,
StockFilter,
StockConsumptionRequest,
StockConsumptionResponse,
PaginatedResponse,
} from '../types/inventory';
export class InventoryService {
private readonly baseUrl = '/tenants';
// Ingredient Management
async createIngredient(
tenantId: string,
ingredientData: IngredientCreate
): Promise<IngredientResponse> {
return apiClient.post<IngredientResponse>(`${this.baseUrl}/${tenantId}/ingredients`, ingredientData);
}
async getIngredient(tenantId: string, ingredientId: string): Promise<IngredientResponse> {
return apiClient.get<IngredientResponse>(`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`);
}
async getIngredients(
tenantId: string,
filter?: InventoryFilter
): Promise<PaginatedResponse<IngredientResponse>> {
const queryParams = new URLSearchParams();
if (filter?.category) queryParams.append('category', filter.category);
if (filter?.stock_status) queryParams.append('stock_status', filter.stock_status);
if (filter?.requires_refrigeration !== undefined)
queryParams.append('requires_refrigeration', filter.requires_refrigeration.toString());
if (filter?.requires_freezing !== undefined)
queryParams.append('requires_freezing', filter.requires_freezing.toString());
if (filter?.is_seasonal !== undefined)
queryParams.append('is_seasonal', filter.is_seasonal.toString());
if (filter?.supplier_id) queryParams.append('supplier_id', filter.supplier_id);
if (filter?.expiring_within_days !== undefined)
queryParams.append('expiring_within_days', filter.expiring_within_days.toString());
if (filter?.search) queryParams.append('search', filter.search);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/ingredients?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/ingredients`;
return apiClient.get<PaginatedResponse<IngredientResponse>>(url);
}
async updateIngredient(
tenantId: string,
ingredientId: string,
updateData: IngredientUpdate
): Promise<IngredientResponse> {
return apiClient.put<IngredientResponse>(
`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`,
updateData
);
}
async deleteIngredient(tenantId: string, ingredientId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}`);
}
async getIngredientsByCategory(tenantId: string): Promise<Record<string, IngredientResponse[]>> {
return apiClient.get<Record<string, IngredientResponse[]>>(`${this.baseUrl}/${tenantId}/ingredients/by-category`);
}
async getLowStockIngredients(tenantId: string): Promise<IngredientResponse[]> {
return apiClient.get<IngredientResponse[]>(`${this.baseUrl}/${tenantId}/ingredients/low-stock`);
}
// Stock Management
async addStock(tenantId: string, stockData: StockCreate): Promise<StockResponse> {
return apiClient.post<StockResponse>(`${this.baseUrl}/${tenantId}/stock`, stockData);
}
async getStock(tenantId: string, stockId: string): Promise<StockResponse> {
return apiClient.get<StockResponse>(`${this.baseUrl}/${tenantId}/stock/${stockId}`);
}
async getStockByIngredient(
tenantId: string,
ingredientId: string,
includeUnavailable: boolean = false
): Promise<StockResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('include_unavailable', includeUnavailable.toString());
return apiClient.get<StockResponse[]>(
`${this.baseUrl}/${tenantId}/stock/ingredient/${ingredientId}?${queryParams.toString()}`
);
}
async getAllStock(tenantId: string, filter?: StockFilter): Promise<PaginatedResponse<StockResponse>> {
const queryParams = new URLSearchParams();
if (filter?.ingredient_id) queryParams.append('ingredient_id', filter.ingredient_id);
if (filter?.is_available !== undefined) queryParams.append('is_available', filter.is_available.toString());
if (filter?.is_expired !== undefined) queryParams.append('is_expired', filter.is_expired.toString());
if (filter?.expiring_within_days !== undefined)
queryParams.append('expiring_within_days', filter.expiring_within_days.toString());
if (filter?.batch_number) queryParams.append('batch_number', filter.batch_number);
if (filter?.supplier_id) queryParams.append('supplier_id', filter.supplier_id);
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
if (filter?.order_by) queryParams.append('order_by', filter.order_by);
if (filter?.order_direction) queryParams.append('order_direction', filter.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/stock?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/stock`;
return apiClient.get<PaginatedResponse<StockResponse>>(url);
}
async updateStock(
tenantId: string,
stockId: string,
updateData: StockUpdate
): Promise<StockResponse> {
return apiClient.put<StockResponse>(`${this.baseUrl}/${tenantId}/stock/${stockId}`, updateData);
}
async deleteStock(tenantId: string, stockId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/stock/${stockId}`);
}
async consumeStock(
tenantId: string,
consumptionData: StockConsumptionRequest
): Promise<StockConsumptionResponse> {
const queryParams = new URLSearchParams();
queryParams.append('ingredient_id', consumptionData.ingredient_id);
queryParams.append('quantity', consumptionData.quantity.toString());
if (consumptionData.reference_number)
queryParams.append('reference_number', consumptionData.reference_number);
if (consumptionData.notes) queryParams.append('notes', consumptionData.notes);
if (consumptionData.fifo !== undefined) queryParams.append('fifo', consumptionData.fifo.toString());
return apiClient.post<StockConsumptionResponse>(
`${this.baseUrl}/${tenantId}/stock/consume?${queryParams.toString()}`
);
}
// Stock Movements
async createStockMovement(
tenantId: string,
movementData: StockMovementCreate
): Promise<StockMovementResponse> {
return apiClient.post<StockMovementResponse>(`${this.baseUrl}/${tenantId}/stock/movements`, movementData);
}
async getStockMovements(
tenantId: string,
ingredientId?: string,
limit: number = 50,
offset: number = 0
): Promise<PaginatedResponse<StockMovementResponse>> {
const queryParams = new URLSearchParams();
if (ingredientId) queryParams.append('ingredient_id', ingredientId);
queryParams.append('limit', limit.toString());
queryParams.append('offset', offset.toString());
return apiClient.get<PaginatedResponse<StockMovementResponse>>(
`${this.baseUrl}/${tenantId}/stock/movements?${queryParams.toString()}`
);
}
// Expiry Management
async getExpiringStock(
tenantId: string,
withinDays: number = 7
): Promise<StockResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('within_days', withinDays.toString());
return apiClient.get<StockResponse[]>(
`${this.baseUrl}/${tenantId}/stock/expiring?${queryParams.toString()}`
);
}
async getExpiredStock(tenantId: string): Promise<StockResponse[]> {
return apiClient.get<StockResponse[]>(`${this.baseUrl}/${tenantId}/stock/expired`);
}
// Analytics
async getStockAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
total_ingredients: number;
total_stock_value: number;
low_stock_count: number;
out_of_stock_count: number;
expiring_soon_count: number;
stock_turnover_rate: number;
}> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/inventory/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/inventory/analytics`;
return apiClient.get(url);
}
}
export const inventoryService = new InventoryService();

View File

@@ -0,0 +1,138 @@
/**
* Inventory Dashboard Service - Mirror backend dashboard endpoints
*/
import { apiClient } from '../client';
import {
InventoryDashboardSummary,
InventoryAnalytics,
BusinessModelInsights,
DashboardFilter,
AlertsFilter,
RecentActivity,
} from '../types/dashboard';
export class InventoryDashboardService {
private readonly baseUrl = '/tenants';
async getDashboardSummary(
tenantId: string,
filter?: DashboardFilter
): Promise<InventoryDashboardSummary> {
const queryParams = new URLSearchParams();
if (filter?.date_range?.start) queryParams.append('start_date', filter.date_range.start);
if (filter?.date_range?.end) queryParams.append('end_date', filter.date_range.end);
if (filter?.categories?.length) queryParams.append('categories', filter.categories.join(','));
if (filter?.include_expired !== undefined)
queryParams.append('include_expired', filter.include_expired.toString());
if (filter?.include_unavailable !== undefined)
queryParams.append('include_unavailable', filter.include_unavailable.toString());
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/summary?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/summary`;
return apiClient.get<InventoryDashboardSummary>(url);
}
async getInventoryAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<InventoryAnalytics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/analytics`;
return apiClient.get<InventoryAnalytics>(url);
}
async getBusinessModelInsights(tenantId: string): Promise<BusinessModelInsights> {
return apiClient.get<BusinessModelInsights>(
`${this.baseUrl}/${tenantId}/dashboard/business-insights`
);
}
async getRecentActivity(
tenantId: string,
limit: number = 20
): Promise<RecentActivity[]> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get<RecentActivity[]>(
`${this.baseUrl}/${tenantId}/dashboard/recent-activity?${queryParams.toString()}`
);
}
async getAlerts(
tenantId: string,
filter?: AlertsFilter
): Promise<{
items: any[];
total: number;
}> {
const queryParams = new URLSearchParams();
if (filter?.severity) queryParams.append('severity', filter.severity);
if (filter?.type) queryParams.append('type', filter.type);
if (filter?.resolved !== undefined) queryParams.append('resolved', filter.resolved.toString());
if (filter?.limit !== undefined) queryParams.append('limit', filter.limit.toString());
if (filter?.offset !== undefined) queryParams.append('offset', filter.offset.toString());
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/dashboard/alerts?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/alerts`;
return apiClient.get(url);
}
async getStockSummary(tenantId: string): Promise<{
in_stock: number;
low_stock: number;
out_of_stock: number;
overstock: number;
total_value: number;
}> {
return apiClient.get(`${this.baseUrl}/${tenantId}/dashboard/stock-summary`);
}
async getTopCategories(tenantId: string, limit: number = 10): Promise<Array<{
category: string;
ingredient_count: number;
total_value: number;
low_stock_count: number;
}>> {
const queryParams = new URLSearchParams();
queryParams.append('limit', limit.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/dashboard/top-categories?${queryParams.toString()}`
);
}
async getExpiryCalendar(
tenantId: string,
daysAhead: number = 30
): Promise<Array<{
date: string;
items: Array<{
ingredient_name: string;
quantity: number;
batch_number?: string;
}>;
}>> {
const queryParams = new URLSearchParams();
queryParams.append('days_ahead', daysAhead.toString());
return apiClient.get(
`${this.baseUrl}/${tenantId}/dashboard/expiry-calendar?${queryParams.toString()}`
);
}
}
export const inventoryDashboardService = new InventoryDashboardService();

View File

@@ -0,0 +1,52 @@
/**
* Onboarding Service - Mirror backend onboarding endpoints
*/
import { apiClient } from '../client';
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
export class OnboardingService {
private readonly baseUrl = '/onboarding';
async getUserProgress(userId: string): Promise<UserProgress> {
return apiClient.get<UserProgress>(`${this.baseUrl}/progress/${userId}`);
}
async updateStep(userId: string, stepData: UpdateStepRequest): Promise<UserProgress> {
return apiClient.put<UserProgress>(`${this.baseUrl}/progress/${userId}/step`, stepData);
}
async markStepCompleted(
userId: string,
stepName: string,
data?: Record<string, any>
): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${this.baseUrl}/progress/${userId}/complete`, {
step_name: stepName,
data: data,
});
}
async resetProgress(userId: string): Promise<UserProgress> {
return apiClient.post<UserProgress>(`${this.baseUrl}/progress/${userId}/reset`);
}
async getStepDetails(stepName: string): Promise<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}> {
return apiClient.get(`${this.baseUrl}/steps/${stepName}`);
}
async getAllSteps(): Promise<Array<{
name: string;
description: string;
dependencies: string[];
estimated_time_minutes: number;
}>> {
return apiClient.get(`${this.baseUrl}/steps`);
}
}
export const onboardingService = new OnboardingService();

View File

@@ -0,0 +1,126 @@
/**
* Sales Service - Mirror backend sales endpoints
*/
import { apiClient } from '../client';
import {
SalesDataCreate,
SalesDataUpdate,
SalesDataResponse,
SalesDataQuery,
SalesAnalytics,
} from '../types/sales';
export class SalesService {
private readonly baseUrl = '/tenants';
// Sales Data CRUD Operations
async createSalesRecord(
tenantId: string,
salesData: SalesDataCreate
): Promise<SalesDataResponse> {
return apiClient.post<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales`, salesData);
}
async getSalesRecords(
tenantId: string,
query?: SalesDataQuery
): Promise<SalesDataResponse[]> {
const queryParams = new URLSearchParams();
if (query?.start_date) queryParams.append('start_date', query.start_date);
if (query?.end_date) queryParams.append('end_date', query.end_date);
if (query?.product_name) queryParams.append('product_name', query.product_name);
if (query?.product_category) queryParams.append('product_category', query.product_category);
if (query?.location_id) queryParams.append('location_id', query.location_id);
if (query?.sales_channel) queryParams.append('sales_channel', query.sales_channel);
if (query?.source) queryParams.append('source', query.source);
if (query?.is_validated !== undefined) queryParams.append('is_validated', query.is_validated.toString());
if (query?.limit !== undefined) queryParams.append('limit', query.limit.toString());
if (query?.offset !== undefined) queryParams.append('offset', query.offset.toString());
if (query?.order_by) queryParams.append('order_by', query.order_by);
if (query?.order_direction) queryParams.append('order_direction', query.order_direction);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales`;
return apiClient.get<SalesDataResponse[]>(url);
}
async getSalesRecord(
tenantId: string,
recordId: string
): Promise<SalesDataResponse> {
return apiClient.get<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales/${recordId}`);
}
async updateSalesRecord(
tenantId: string,
recordId: string,
updateData: SalesDataUpdate
): Promise<SalesDataResponse> {
return apiClient.put<SalesDataResponse>(`${this.baseUrl}/${tenantId}/sales/${recordId}`, updateData);
}
async deleteSalesRecord(
tenantId: string,
recordId: string
): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${tenantId}/sales/${recordId}`);
}
async validateSalesRecord(
tenantId: string,
recordId: string,
validationNotes?: string
): Promise<SalesDataResponse> {
const queryParams = new URLSearchParams();
if (validationNotes) queryParams.append('validation_notes', validationNotes);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales/${recordId}/validate?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales/${recordId}/validate`;
return apiClient.post<SalesDataResponse>(url);
}
// Analytics & Reporting
async getSalesAnalytics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<SalesAnalytics> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/sales/analytics/summary?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/sales/analytics/summary`;
return apiClient.get<SalesAnalytics>(url);
}
async getProductSales(
tenantId: string,
inventoryProductId: string,
startDate?: string,
endDate?: string
): Promise<SalesDataResponse[]> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales`;
return apiClient.get<SalesDataResponse[]>(url);
}
async getProductCategories(tenantId: string): Promise<string[]> {
return apiClient.get<string[]>(`${this.baseUrl}/${tenantId}/sales/categories`);
}
}
export const salesService = new SalesService();

View File

@@ -0,0 +1,67 @@
/**
* Subscription Service - Mirror backend subscription endpoints
*/
import { apiClient } from '../client';
import {
SubscriptionLimits,
FeatureCheckRequest,
FeatureCheckResponse,
UsageCheckRequest,
UsageCheckResponse
} from '../types/subscription';
export class SubscriptionService {
private readonly baseUrl = '/subscriptions';
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
return apiClient.get<SubscriptionLimits>(`${this.baseUrl}/${tenantId}/limits`);
}
async checkFeatureAccess(
tenantId: string,
featureName: string
): Promise<FeatureCheckResponse> {
return apiClient.get<FeatureCheckResponse>(
`${this.baseUrl}/${tenantId}/features/${featureName}/check`
);
}
async checkUsageLimit(
tenantId: string,
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
requestedAmount?: number
): Promise<UsageCheckResponse> {
const queryParams = new URLSearchParams();
if (requestedAmount !== undefined) {
queryParams.append('requested_amount', requestedAmount.toString());
}
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/usage/${resourceType}/check?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/usage/${resourceType}/check`;
return apiClient.get<UsageCheckResponse>(url);
}
async recordUsage(
tenantId: string,
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
amount: number = 1
): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(
`${this.baseUrl}/${tenantId}/usage/${resourceType}/record`,
{ amount }
);
}
async getCurrentUsage(tenantId: string): Promise<{
users: number;
sales_records: number;
inventory_items: number;
api_requests_this_hour: number;
}> {
return apiClient.get(`${this.baseUrl}/${tenantId}/usage/current`);
}
}
export const subscriptionService = new SubscriptionService();

View File

@@ -0,0 +1,145 @@
/**
* Tenant Service - Mirror backend tenant endpoints
*/
import { apiClient } from '../client';
import {
BakeryRegistration,
TenantResponse,
TenantAccessResponse,
TenantUpdate,
TenantMemberResponse,
TenantStatistics,
TenantSearchParams,
TenantNearbyParams,
} from '../types/tenant';
export class TenantService {
private readonly baseUrl = '/tenants';
// Tenant CRUD Operations
async registerBakery(bakeryData: BakeryRegistration): Promise<TenantResponse> {
return apiClient.post<TenantResponse>(`${this.baseUrl}/register`, bakeryData);
}
async getTenant(tenantId: string): Promise<TenantResponse> {
return apiClient.get<TenantResponse>(`${this.baseUrl}/${tenantId}`);
}
async getTenantBySubdomain(subdomain: string): Promise<TenantResponse> {
return apiClient.get<TenantResponse>(`${this.baseUrl}/subdomain/${subdomain}`);
}
async getUserTenants(userId: string): Promise<TenantResponse[]> {
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/users/${userId}`);
}
async getUserOwnedTenants(userId: string): Promise<TenantResponse[]> {
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/user/${userId}/owned`);
}
async updateTenant(tenantId: string, updateData: TenantUpdate): Promise<TenantResponse> {
return apiClient.put<TenantResponse>(`${this.baseUrl}/${tenantId}`, updateData);
}
async deactivateTenant(tenantId: string): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/deactivate`);
}
async activateTenant(tenantId: string): Promise<{ success: boolean; message: string }> {
return apiClient.post<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/activate`);
}
// Access Control
async verifyTenantAccess(tenantId: string, userId: string): Promise<TenantAccessResponse> {
return apiClient.get<TenantAccessResponse>(`${this.baseUrl}/${tenantId}/access/${userId}`);
}
// Search & Discovery
async searchTenants(params: TenantSearchParams): Promise<TenantResponse[]> {
const queryParams = new URLSearchParams();
if (params.search_term) queryParams.append('search_term', params.search_term);
if (params.business_type) queryParams.append('business_type', params.business_type);
if (params.city) queryParams.append('city', params.city);
if (params.skip !== undefined) queryParams.append('skip', params.skip.toString());
if (params.limit !== undefined) queryParams.append('limit', params.limit.toString());
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/search?${queryParams.toString()}`);
}
async getNearbyTenants(params: TenantNearbyParams): Promise<TenantResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('latitude', params.latitude.toString());
queryParams.append('longitude', params.longitude.toString());
if (params.radius_km !== undefined) queryParams.append('radius_km', params.radius_km.toString());
if (params.limit !== undefined) queryParams.append('limit', params.limit.toString());
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/nearby?${queryParams.toString()}`);
}
// Model Management
async updateModelStatus(
tenantId: string,
modelTrained: boolean,
lastTrainingDate?: string
): Promise<TenantResponse> {
const queryParams = new URLSearchParams();
queryParams.append('model_trained', modelTrained.toString());
if (lastTrainingDate) queryParams.append('last_training_date', lastTrainingDate);
return apiClient.put<TenantResponse>(`${this.baseUrl}/${tenantId}/model-status?${queryParams.toString()}`);
}
// Team Management
async addTeamMember(
tenantId: string,
userId: string,
role: string
): Promise<TenantMemberResponse> {
return apiClient.post<TenantMemberResponse>(`${this.baseUrl}/${tenantId}/members`, {
user_id: userId,
role: role,
});
}
async getTeamMembers(tenantId: string, activeOnly: boolean = true): Promise<TenantMemberResponse[]> {
const queryParams = new URLSearchParams();
queryParams.append('active_only', activeOnly.toString());
return apiClient.get<TenantMemberResponse[]>(`${this.baseUrl}/${tenantId}/members?${queryParams.toString()}`);
}
async updateMemberRole(
tenantId: string,
memberUserId: string,
newRole: string
): Promise<TenantMemberResponse> {
return apiClient.put<TenantMemberResponse>(
`${this.baseUrl}/${tenantId}/members/${memberUserId}/role`,
{ new_role: newRole }
);
}
async removeTeamMember(tenantId: string, memberUserId: string): Promise<{ success: boolean; message: string }> {
return apiClient.delete<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/members/${memberUserId}`);
}
// Admin Operations
async getTenantStatistics(): Promise<TenantStatistics> {
return apiClient.get<TenantStatistics>(`${this.baseUrl}/statistics`);
}
// Context Management (Frontend-only operations)
setCurrentTenant(tenant: TenantResponse): void {
// Set tenant context in API client
apiClient.setTenantId(tenant.id);
}
clearCurrentTenant(): void {
// Clear tenant context from API client
apiClient.setTenantId(null);
}
}
export const tenantService = new TenantService();

View File

@@ -0,0 +1,37 @@
/**
* User Service - Mirror backend user endpoints
*/
import { apiClient } from '../client';
import { UserResponse, UserUpdate } from '../types/auth';
import { AdminDeleteRequest, AdminDeleteResponse } from '../types/user';
export class UserService {
private readonly baseUrl = '/users';
async getCurrentUser(): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/me`);
}
async updateUser(userId: string, updateData: UserUpdate): Promise<UserResponse> {
return apiClient.put<UserResponse>(`${this.baseUrl}/${userId}`, updateData);
}
async deleteUser(userId: string): Promise<{ message: string }> {
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${userId}`);
}
// Admin operations
async adminDeleteUser(deleteRequest: AdminDeleteRequest): Promise<AdminDeleteResponse> {
return apiClient.post<AdminDeleteResponse>(`${this.baseUrl}/admin/delete`, deleteRequest);
}
async getAllUsers(): Promise<UserResponse[]> {
return apiClient.get<UserResponse[]>(`${this.baseUrl}/admin/all`);
}
async getUserById(userId: string): Promise<UserResponse> {
return apiClient.get<UserResponse>(`${this.baseUrl}/admin/${userId}`);
}
}
export const userService = new UserService();