Start integrating the onboarding flow with backend 4
This commit is contained in:
@@ -10,7 +10,9 @@ import {
|
||||
PasswordResetConfirm,
|
||||
TokenVerification,
|
||||
UserResponse,
|
||||
UserUpdate
|
||||
UserUpdate,
|
||||
OnboardingStatus,
|
||||
OnboardingProgressRequest
|
||||
} from '../../types/auth.types';
|
||||
|
||||
class AuthService {
|
||||
@@ -200,6 +202,71 @@ class AuthService {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Onboarding progress tracking (moved from onboarding)
|
||||
async checkOnboardingStatus(): Promise<ApiResponse<OnboardingStatus>> {
|
||||
try {
|
||||
// Use the /me endpoint which gets proxied to auth service
|
||||
const response = await apiClient.get<any>('/me');
|
||||
|
||||
if (response.success && response.data) {
|
||||
// Extract onboarding status from user profile
|
||||
const onboardingStatus = {
|
||||
completed: response.data.onboarding_completed || false,
|
||||
steps_completed: response.data.completed_steps || []
|
||||
};
|
||||
return {
|
||||
success: true,
|
||||
data: onboardingStatus,
|
||||
message: 'Onboarding status retrieved successfully'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
data: { completed: false, steps_completed: [] },
|
||||
message: 'Could not retrieve onboarding status',
|
||||
error: 'Invalid response data'
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Could not check onboarding status:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: { completed: false, steps_completed: [] },
|
||||
message: 'Could not retrieve onboarding status',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async completeOnboarding(metadata?: any): Promise<ApiResponse<{ message: string }>> {
|
||||
try {
|
||||
// Update user profile to mark onboarding as complete
|
||||
const response = await apiClient.patch<any>('/me', {
|
||||
onboarding_completed: true,
|
||||
completed_steps: ['setup', 'data-processing', 'review', 'inventory', 'suppliers', 'ml-training', 'completion'],
|
||||
onboarding_metadata: metadata
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Onboarding completed successfully' },
|
||||
message: 'Onboarding marked as complete'
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.warn('Could not mark onboarding as complete:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: { message: 'Failed to complete onboarding' },
|
||||
message: 'Could not complete onboarding',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
@@ -56,6 +56,31 @@ class ApiClient {
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
// Helper method to build tenant-scoped URLs
|
||||
private buildTenantUrl(path: string): string {
|
||||
// If path already starts with /tenants, return as-is
|
||||
if (path.startsWith('/tenants/')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// If it's an auth endpoint, return as-is
|
||||
if (path.startsWith('/auth')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Get tenant ID from stores
|
||||
const tenantData = getTenantData();
|
||||
const authData = getAuthData();
|
||||
const tenantId = tenantData?.currentTenant?.id || authData?.user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
throw new Error('Tenant ID not available for API call');
|
||||
}
|
||||
|
||||
// Build tenant-scoped URL: /tenants/{tenant-id}{original-path}
|
||||
return `/tenants/${tenantId}${path}`;
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
// Request interceptor - add auth token and tenant ID
|
||||
this.axiosInstance.interceptors.request.use(
|
||||
@@ -168,33 +193,38 @@ class ApiClient {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// HTTP Methods with consistent response format
|
||||
// HTTP Methods with consistent response format and automatic tenant scoping
|
||||
async get<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.get(url, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.get(tenantScopedUrl, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async post<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.post(url, data, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.post(tenantScopedUrl, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async put<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.put(url, data, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.put(tenantScopedUrl, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async patch<T = any>(url: string, data = {}, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.patch(url, data, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.patch(tenantScopedUrl, data, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
async delete<T = any>(url: string, config = {}): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.delete(url, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.delete(tenantScopedUrl, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
// File upload helper
|
||||
// File upload helper with automatic tenant scoping
|
||||
async uploadFile<T = any>(url: string, file: File, progressCallback?: (progress: number) => void): Promise<ApiResponse<T>> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -211,7 +241,8 @@ class ApiClient {
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.axiosInstance.post(url, formData, config);
|
||||
const tenantScopedUrl = this.buildTenantUrl(url);
|
||||
const response = await this.axiosInstance.post(tenantScopedUrl, formData, config);
|
||||
return this.transformResponse(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -264,4 +264,5 @@ class ForecastingService {
|
||||
}
|
||||
}
|
||||
|
||||
export { ForecastingService };
|
||||
export const forecastingService = new ForecastingService();
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
StockMovement,
|
||||
StockAlert,
|
||||
InventorySummary,
|
||||
StockLevelSummary
|
||||
StockLevelSummary,
|
||||
ProductSuggestion,
|
||||
ProductSuggestionsResponse,
|
||||
InventoryCreationResponse,
|
||||
BatchClassificationRequest
|
||||
} from '../../types/inventory.types';
|
||||
import { PaginatedResponse } from '../../types/api.types';
|
||||
|
||||
@@ -390,6 +394,92 @@ class InventoryService {
|
||||
{ value: 'quarantine', label: 'Quarantine', color: 'purple' },
|
||||
];
|
||||
}
|
||||
|
||||
// AI-powered inventory classification and suggestions (moved from onboarding)
|
||||
async generateInventorySuggestions(
|
||||
productList: string[]
|
||||
): Promise<ApiResponse<ProductSuggestionsResponse>> {
|
||||
try {
|
||||
if (!productList || !Array.isArray(productList) || productList.length === 0) {
|
||||
throw new Error('Product list is empty or invalid');
|
||||
}
|
||||
|
||||
// Transform product list into the expected format for BatchClassificationRequest
|
||||
const products = productList.map(productName => ({
|
||||
product_name: productName,
|
||||
sales_data: {} // Additional context can be added later
|
||||
}));
|
||||
|
||||
const requestData: BatchClassificationRequest = {
|
||||
products: products
|
||||
};
|
||||
|
||||
const response = await apiClient.post<ProductSuggestionsResponse>(
|
||||
`${this.baseUrl}/classify-products-batch`,
|
||||
requestData
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Suggestion generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createInventoryFromSuggestions(
|
||||
approvedSuggestions: ProductSuggestion[]
|
||||
): Promise<ApiResponse<InventoryCreationResponse>> {
|
||||
try {
|
||||
const createdItems: any[] = [];
|
||||
const failedItems: any[] = [];
|
||||
const inventoryMapping: { [productName: string]: string } = {};
|
||||
|
||||
// Create inventory items one by one using inventory service
|
||||
for (const suggestion of approvedSuggestions) {
|
||||
try {
|
||||
const ingredientData = {
|
||||
name: suggestion.suggested_name,
|
||||
category: suggestion.category,
|
||||
unit_of_measure: suggestion.unit_of_measure,
|
||||
shelf_life_days: suggestion.estimated_shelf_life_days,
|
||||
requires_refrigeration: suggestion.requires_refrigeration,
|
||||
requires_freezing: suggestion.requires_freezing,
|
||||
is_seasonal: suggestion.is_seasonal,
|
||||
product_type: suggestion.product_type
|
||||
};
|
||||
|
||||
const response = await apiClient.post<any>(
|
||||
'/ingredients',
|
||||
ingredientData
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
createdItems.push(response.data);
|
||||
inventoryMapping[suggestion.original_name] = response.data.id;
|
||||
} else {
|
||||
failedItems.push({ suggestion, error: response.error });
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
failedItems.push({ suggestion, error: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
created_items: createdItems,
|
||||
failed_items: failedItems,
|
||||
total_approved: approvedSuggestions.length,
|
||||
success_rate: createdItems.length / approvedSuggestions.length,
|
||||
inventory_mapping: inventoryMapping
|
||||
};
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
console.error('Inventory creation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { InventoryService };
|
||||
export const inventoryService = new InventoryService();
|
||||
@@ -1,496 +0,0 @@
|
||||
/**
|
||||
* Enhanced Onboarding API Service
|
||||
* Provides integration with backend AI-powered onboarding endpoints
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface OnboardingFileValidationResponse {
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
unique_products: number;
|
||||
product_list: string[];
|
||||
validation_errors: any[];
|
||||
validation_warnings: any[];
|
||||
summary: any;
|
||||
}
|
||||
|
||||
export interface ProductSuggestion {
|
||||
suggestion_id: string;
|
||||
original_name: string;
|
||||
suggested_name: string;
|
||||
product_type: 'ingredient' | 'finished_product';
|
||||
category: string;
|
||||
unit_of_measure: string;
|
||||
confidence_score: number;
|
||||
estimated_shelf_life_days: number;
|
||||
requires_refrigeration: boolean;
|
||||
requires_freezing: boolean;
|
||||
is_seasonal: boolean;
|
||||
suggested_supplier?: string;
|
||||
notes: string;
|
||||
sales_data: {
|
||||
total_quantity: number;
|
||||
average_daily_sales: number;
|
||||
peak_day: string;
|
||||
frequency: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BusinessModelAnalysis {
|
||||
model: 'production' | 'retail' | 'hybrid';
|
||||
confidence: number;
|
||||
ingredient_count: number;
|
||||
finished_product_count: number;
|
||||
ingredient_ratio: number;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface ProductSuggestionsResponse {
|
||||
suggestions: ProductSuggestion[];
|
||||
business_model_analysis: BusinessModelAnalysis;
|
||||
total_products: number;
|
||||
high_confidence_count: number;
|
||||
low_confidence_count: number;
|
||||
processing_time_seconds: number;
|
||||
}
|
||||
|
||||
export interface InventoryCreationResponse {
|
||||
created_items: any[];
|
||||
failed_items: any[];
|
||||
total_approved: number;
|
||||
success_rate: number;
|
||||
inventory_mapping?: { [productName: string]: string };
|
||||
}
|
||||
|
||||
export interface SalesImportResponse {
|
||||
import_job_id: string;
|
||||
status: 'completed' | 'failed' | 'partial';
|
||||
processed_rows: number;
|
||||
successful_imports: number;
|
||||
failed_imports: number;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
processing_time?: number;
|
||||
}
|
||||
|
||||
export interface BusinessModelGuide {
|
||||
title: string;
|
||||
description: string;
|
||||
next_steps: string[];
|
||||
recommended_features: string[];
|
||||
sample_workflows: string[];
|
||||
}
|
||||
|
||||
class OnboardingApiService {
|
||||
private readonly basePath = '/tenants';
|
||||
private readonly salesBasePath = '/tenants';
|
||||
|
||||
/**
|
||||
* Step 1: Validate uploaded file and extract unique products
|
||||
* Now uses Sales Service directly
|
||||
*/
|
||||
async validateOnboardingFile(
|
||||
tenantId: string,
|
||||
file: File
|
||||
): Promise<OnboardingFileValidationResponse> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await apiClient.post<OnboardingFileValidationResponse>(
|
||||
`${this.salesBasePath}/${tenantId}/sales/import/validate`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Validation failed: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('File validation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Generate AI-powered inventory suggestions
|
||||
* Now uses Inventory Service directly
|
||||
*/
|
||||
async generateInventorySuggestions(
|
||||
tenantId: string,
|
||||
file: File,
|
||||
productList: string[]
|
||||
): Promise<ProductSuggestionsResponse> {
|
||||
try {
|
||||
if (!productList || !Array.isArray(productList) || productList.length === 0) {
|
||||
throw new Error('Product list is empty or invalid');
|
||||
}
|
||||
|
||||
// Transform product list into the expected format for BatchClassificationRequest
|
||||
const products = productList.map(productName => ({
|
||||
product_name: productName,
|
||||
// sales_volume is optional, omit it if we don't have the data
|
||||
sales_data: {} // Additional context can be added later
|
||||
}));
|
||||
|
||||
const requestData = {
|
||||
products: products
|
||||
};
|
||||
|
||||
const response = await apiClient.post<ProductSuggestionsResponse>(
|
||||
`${this.basePath}/${tenantId}/inventory/classify-products-batch`,
|
||||
requestData
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Suggestion generation failed: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Suggestion generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Create inventory items from approved suggestions
|
||||
* Now uses Inventory Service directly
|
||||
*/
|
||||
async createInventoryFromSuggestions(
|
||||
tenantId: string,
|
||||
approvedSuggestions: any[]
|
||||
): Promise<InventoryCreationResponse> {
|
||||
try {
|
||||
const createdItems: any[] = [];
|
||||
const failedItems: any[] = [];
|
||||
const inventoryMapping: { [productName: string]: string } = {};
|
||||
|
||||
// Create inventory items one by one using inventory service
|
||||
for (const suggestion of approvedSuggestions) {
|
||||
try {
|
||||
const ingredientData = {
|
||||
name: suggestion.suggested_name,
|
||||
category: suggestion.category,
|
||||
unit_of_measure: suggestion.unit_of_measure,
|
||||
shelf_life_days: suggestion.estimated_shelf_life_days,
|
||||
requires_refrigeration: suggestion.requires_refrigeration,
|
||||
requires_freezing: suggestion.requires_freezing,
|
||||
is_seasonal: suggestion.is_seasonal,
|
||||
product_type: suggestion.product_type
|
||||
};
|
||||
|
||||
const response = await apiClient.post<any>(
|
||||
`${this.basePath}/${tenantId}/ingredients`,
|
||||
ingredientData
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
createdItems.push(response.data);
|
||||
inventoryMapping[suggestion.original_name] = response.data.id;
|
||||
} else {
|
||||
failedItems.push({ suggestion, error: response.error });
|
||||
}
|
||||
} catch (error) {
|
||||
failedItems.push({ suggestion, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
created_items: createdItems,
|
||||
failed_items: failedItems,
|
||||
total_approved: approvedSuggestions.length,
|
||||
success_rate: createdItems.length / approvedSuggestions.length,
|
||||
inventory_mapping: inventoryMapping
|
||||
};
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Inventory creation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4: Import sales data with inventory mapping
|
||||
* Now uses Sales Service directly with validation first
|
||||
*/
|
||||
async importSalesWithInventory(
|
||||
tenantId: string,
|
||||
file: File,
|
||||
inventoryMapping: { [productName: string]: string }
|
||||
): Promise<SalesImportResponse> {
|
||||
try {
|
||||
// First validate the file with inventory mapping
|
||||
await this.validateSalesData(tenantId, file);
|
||||
|
||||
// Then import the sales data
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('update_existing', 'true');
|
||||
|
||||
const response = await apiClient.post<SalesImportResponse>(
|
||||
`${this.salesBasePath}/${tenantId}/sales/import`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Sales import failed: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Sales import failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get business model specific recommendations
|
||||
* Returns static recommendations since orchestration is removed
|
||||
*/
|
||||
async getBusinessModelGuide(
|
||||
tenantId: string,
|
||||
model: 'production' | 'retail' | 'hybrid'
|
||||
): Promise<BusinessModelGuide> {
|
||||
// Return static business model guides since we removed orchestration
|
||||
const guides = {
|
||||
production: {
|
||||
title: 'Production Bakery Setup',
|
||||
description: 'Your bakery focuses on creating products from raw ingredients.',
|
||||
next_steps: [
|
||||
'Set up ingredient inventory management',
|
||||
'Configure recipe management',
|
||||
'Set up production planning',
|
||||
'Implement quality control processes'
|
||||
],
|
||||
recommended_features: [
|
||||
'Inventory tracking for raw ingredients',
|
||||
'Recipe costing and management',
|
||||
'Production scheduling',
|
||||
'Supplier management'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Daily production planning based on demand forecasts',
|
||||
'Inventory reordering based on production schedules',
|
||||
'Quality control checkpoints during production'
|
||||
]
|
||||
},
|
||||
retail: {
|
||||
title: 'Retail Bakery Setup',
|
||||
description: 'Your bakery focuses on selling finished products to customers.',
|
||||
next_steps: [
|
||||
'Set up finished product inventory',
|
||||
'Configure point-of-sale integration',
|
||||
'Set up customer management',
|
||||
'Implement sales analytics'
|
||||
],
|
||||
recommended_features: [
|
||||
'Finished product inventory tracking',
|
||||
'Sales analytics and reporting',
|
||||
'Customer loyalty programs',
|
||||
'Promotional campaign management'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Daily sales reporting and analysis',
|
||||
'Inventory reordering based on sales velocity',
|
||||
'Customer engagement and retention campaigns'
|
||||
]
|
||||
},
|
||||
hybrid: {
|
||||
title: 'Hybrid Bakery Setup',
|
||||
description: 'Your bakery combines production and retail operations.',
|
||||
next_steps: [
|
||||
'Set up both ingredient and finished product inventory',
|
||||
'Configure production-to-retail workflows',
|
||||
'Set up integrated analytics',
|
||||
'Implement comprehensive supplier management'
|
||||
],
|
||||
recommended_features: [
|
||||
'Dual inventory management system',
|
||||
'Production-to-sales analytics',
|
||||
'Integrated supplier and customer management',
|
||||
'Cross-channel reporting'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Production planning based on both wholesale and retail demand',
|
||||
'Integrated inventory management across production and retail',
|
||||
'Comprehensive business intelligence and reporting'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return guides[model] || guides.hybrid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sales data using the sales service (fallback)
|
||||
*/
|
||||
async validateSalesData(
|
||||
tenantId: string,
|
||||
file: File
|
||||
): Promise<any> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await apiClient.post<any>(
|
||||
`${this.salesBasePath}/${tenantId}/sales/import/validate`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Sales validation failed: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Sales validation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import sales data using the sales service (fallback)
|
||||
*/
|
||||
async importSalesData(
|
||||
tenantId: string,
|
||||
file: File,
|
||||
updateExisting: boolean = false
|
||||
): Promise<any> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('update_existing', updateExisting.toString());
|
||||
|
||||
const response = await apiClient.post<any>(
|
||||
`${this.salesBasePath}/${tenantId}/sales/import`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Sales import failed: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Sales import failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sales data import template
|
||||
*/
|
||||
async getSalesImportTemplate(
|
||||
tenantId: string,
|
||||
format: 'csv' | 'json' = 'csv'
|
||||
): Promise<any> {
|
||||
try {
|
||||
const response = await apiClient.get<any>(
|
||||
`${this.salesBasePath}/${tenantId}/sales/import/template?format=${format}`
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Failed to get template: ${response.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get template:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download template file (utility method)
|
||||
*/
|
||||
downloadTemplate(templateData: any, filename: string, format: 'csv' | 'json' = 'csv'): void {
|
||||
let content: string;
|
||||
let mimeType: string;
|
||||
|
||||
if (format === 'csv') {
|
||||
content = templateData.template;
|
||||
mimeType = 'text/csv';
|
||||
} else {
|
||||
content = JSON.stringify(templateData.template, null, 2);
|
||||
mimeType = 'application/json';
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Check if a tenant has completed onboarding
|
||||
* Uses Auth Service for user progress tracking
|
||||
*/
|
||||
async checkOnboardingStatus(tenantId: string): Promise<{ completed: boolean; steps_completed: string[] }> {
|
||||
try {
|
||||
const response = await apiClient.get<any>(
|
||||
'/me/onboarding/progress'
|
||||
);
|
||||
|
||||
return {
|
||||
completed: response.data?.onboarding_completed || false,
|
||||
steps_completed: response.data?.completed_steps || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Could not check onboarding status:', error);
|
||||
return { completed: false, steps_completed: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Mark onboarding as complete
|
||||
* Uses Auth Service for user progress tracking
|
||||
*/
|
||||
async completeOnboarding(tenantId: string, metadata?: any): Promise<void> {
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/me/onboarding/complete',
|
||||
{ metadata }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Could not mark onboarding as complete:', error);
|
||||
// Don't throw error, this is not critical
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const onboardingApiService = new OnboardingApiService();
|
||||
export default OnboardingApiService;
|
||||
1
frontend/src/services/api/order.service.ts
Symbolic link
1
frontend/src/services/api/order.service.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
/Users/urtzialfaro/Documents/bakery-ia/frontend/src/services/api/orders.service.ts
|
||||
@@ -187,4 +187,6 @@ class OrdersService {
|
||||
}
|
||||
}
|
||||
|
||||
export { OrdersService };
|
||||
export { OrdersService as OrderService }; // Alias for compatibility
|
||||
export const ordersService = new OrdersService();
|
||||
@@ -37,8 +37,7 @@ class ProcurementService {
|
||||
}
|
||||
|
||||
private getBaseUrl(): string {
|
||||
const tenantId = this.getTenantId();
|
||||
return `/tenants/${tenantId}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Purchase Order management
|
||||
@@ -216,4 +215,5 @@ class ProcurementService {
|
||||
|
||||
}
|
||||
|
||||
export { ProcurementService };
|
||||
export const procurementService = new ProcurementService();
|
||||
@@ -464,4 +464,5 @@ class ProductionService {
|
||||
}
|
||||
}
|
||||
|
||||
export { ProductionService };
|
||||
export const productionService = new ProductionService();
|
||||
@@ -1,4 +1,6 @@
|
||||
import { apiClient, ApiResponse } from './client';
|
||||
import { apiClient } from './client';
|
||||
import { ApiResponse } from '../../types/api.types';
|
||||
import { BusinessModelGuide, BusinessModelType, TemplateData } from '../../types/sales.types';
|
||||
|
||||
// Request/Response Types
|
||||
export interface SalesData {
|
||||
@@ -262,23 +264,44 @@ class SalesService {
|
||||
}
|
||||
|
||||
// Data import and export
|
||||
async importSalesData(file: File, progressCallback?: (progress: number) => void): Promise<ApiResponse<SalesImportResult>> {
|
||||
return apiClient.uploadFile(`${this.baseUrl}/import`, file, progressCallback);
|
||||
async importSalesData(file: File, progressCallback?: (progress: number) => void): Promise<{
|
||||
status: 'completed' | 'failed' | 'partial';
|
||||
records_processed: number;
|
||||
records_created: number;
|
||||
records_failed: number;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
processing_time?: number;
|
||||
}> {
|
||||
const response = await apiClient.uploadFile(`${this.baseUrl}/import`, file, progressCallback);
|
||||
if (!response.success) {
|
||||
throw new Error(`Sales import failed: ${response.error || response.detail || 'Unknown error'}`);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async validateSalesData(file: File): Promise<ApiResponse<{
|
||||
valid_records: number;
|
||||
invalid_records: number;
|
||||
errors: Array<{
|
||||
row: number;
|
||||
field: string;
|
||||
message: string;
|
||||
}>;
|
||||
preview: SalesData[];
|
||||
}>> {
|
||||
return apiClient.uploadFile(`${this.baseUrl}/validate`, file);
|
||||
async validateSalesData(file: File): Promise<{
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
unique_products: number;
|
||||
product_list: string[];
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
summary: {
|
||||
date_range: string;
|
||||
total_sales: number;
|
||||
average_daily_sales: number;
|
||||
};
|
||||
}> {
|
||||
const response = await apiClient.uploadFile(`${this.baseUrl}/import/validate`, file);
|
||||
if (!response.success) {
|
||||
throw new Error(`Validation failed: ${response.error || response.detail || 'Unknown error'}`);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async exportSalesData(params?: {
|
||||
format?: 'csv' | 'xlsx';
|
||||
start_date?: string;
|
||||
@@ -438,6 +461,110 @@ class SalesService {
|
||||
{ value: 'xlsx', label: 'Excel (XLSX)' },
|
||||
];
|
||||
}
|
||||
|
||||
// Business model guidance (moved from onboarding)
|
||||
async getBusinessModelGuide(
|
||||
model: BusinessModelType
|
||||
): Promise<ApiResponse<BusinessModelGuide>> {
|
||||
// Return static business model guides since we removed orchestration
|
||||
const guides = {
|
||||
[BusinessModelType.PRODUCTION]: {
|
||||
title: 'Production Bakery Setup',
|
||||
description: 'Your bakery focuses on creating products from raw ingredients.',
|
||||
next_steps: [
|
||||
'Set up ingredient inventory management',
|
||||
'Configure recipe management',
|
||||
'Set up production planning',
|
||||
'Implement quality control processes'
|
||||
],
|
||||
recommended_features: [
|
||||
'Inventory tracking for raw ingredients',
|
||||
'Recipe costing and management',
|
||||
'Production scheduling',
|
||||
'Supplier management'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Daily production planning based on demand forecasts',
|
||||
'Inventory reordering based on production schedules',
|
||||
'Quality control checkpoints during production'
|
||||
]
|
||||
},
|
||||
[BusinessModelType.RETAIL]: {
|
||||
title: 'Retail Bakery Setup',
|
||||
description: 'Your bakery focuses on selling finished products to customers.',
|
||||
next_steps: [
|
||||
'Set up finished product inventory',
|
||||
'Configure point-of-sale integration',
|
||||
'Set up customer management',
|
||||
'Implement sales analytics'
|
||||
],
|
||||
recommended_features: [
|
||||
'Finished product inventory tracking',
|
||||
'Sales analytics and reporting',
|
||||
'Customer loyalty programs',
|
||||
'Promotional campaign management'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Daily sales reporting and analysis',
|
||||
'Inventory reordering based on sales velocity',
|
||||
'Customer engagement and retention campaigns'
|
||||
]
|
||||
},
|
||||
[BusinessModelType.HYBRID]: {
|
||||
title: 'Hybrid Bakery Setup',
|
||||
description: 'Your bakery combines production and retail operations.',
|
||||
next_steps: [
|
||||
'Set up both ingredient and finished product inventory',
|
||||
'Configure production-to-retail workflows',
|
||||
'Set up integrated analytics',
|
||||
'Implement comprehensive supplier management'
|
||||
],
|
||||
recommended_features: [
|
||||
'Dual inventory management system',
|
||||
'Production-to-sales analytics',
|
||||
'Integrated supplier and customer management',
|
||||
'Cross-channel reporting'
|
||||
],
|
||||
sample_workflows: [
|
||||
'Production planning based on both wholesale and retail demand',
|
||||
'Integrated inventory management across production and retail',
|
||||
'Comprehensive business intelligence and reporting'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const guide = guides[model] || guides[BusinessModelType.HYBRID];
|
||||
return { success: true, data: guide, message: 'Business model guide retrieved successfully' };
|
||||
}
|
||||
|
||||
// Template download utility (moved from onboarding)
|
||||
downloadTemplate(templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv'): void {
|
||||
let content: string;
|
||||
let mimeType: string;
|
||||
|
||||
if (format === 'csv') {
|
||||
content = typeof templateData.template === 'string' ? templateData.template : JSON.stringify(templateData.template);
|
||||
mimeType = 'text/csv';
|
||||
} else {
|
||||
content = JSON.stringify(templateData.template, null, 2);
|
||||
mimeType = 'application/json';
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
export { SalesService };
|
||||
export const salesService = new SalesService();
|
||||
@@ -25,8 +25,7 @@ export class TrainingService {
|
||||
}
|
||||
|
||||
private getBaseUrl(): string {
|
||||
const tenantId = this.getTenantId();
|
||||
return `/tenants/${tenantId}/training`;
|
||||
return '/training';
|
||||
}
|
||||
|
||||
async getTrainingJobs(modelId?: string): Promise<ApiResponse<TrainingJob[]>> {
|
||||
|
||||
397
frontend/src/services/api/utils/storage.service.ts
Normal file
397
frontend/src/services/api/utils/storage.service.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* Storage Service - Provides secure and consistent local/session storage management
|
||||
* with encryption, expiration, and type safety
|
||||
*/
|
||||
|
||||
interface StorageOptions {
|
||||
encrypt?: boolean;
|
||||
expiresIn?: number; // milliseconds
|
||||
storage?: 'local' | 'session';
|
||||
}
|
||||
|
||||
interface StorageItem<T = any> {
|
||||
value: T;
|
||||
encrypted?: boolean;
|
||||
expiresAt?: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
class StorageService {
|
||||
private readonly encryptionKey = 'bakery-app-key'; // In production, use proper key management
|
||||
|
||||
/**
|
||||
* Store data in browser storage
|
||||
*/
|
||||
setItem<T>(key: string, value: T, options: StorageOptions = {}): boolean {
|
||||
try {
|
||||
const {
|
||||
encrypt = false,
|
||||
expiresIn,
|
||||
storage = 'local'
|
||||
} = options;
|
||||
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
const item: StorageItem = {
|
||||
value: encrypt ? this.encrypt(JSON.stringify(value)) : value,
|
||||
encrypted: encrypt,
|
||||
createdAt: Date.now(),
|
||||
...(expiresIn && { expiresAt: Date.now() + expiresIn })
|
||||
};
|
||||
|
||||
storageInstance.setItem(key, JSON.stringify(item));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage error setting item "${key}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from browser storage
|
||||
*/
|
||||
getItem<T>(key: string, storage: 'local' | 'session' = 'local'): T | null {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const itemStr = storageInstance.getItem(key);
|
||||
|
||||
if (!itemStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||
|
||||
// Check expiration
|
||||
if (item.expiresAt && Date.now() > item.expiresAt) {
|
||||
this.removeItem(key, storage);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle encrypted data
|
||||
if (item.encrypted && typeof item.value === 'string') {
|
||||
try {
|
||||
const decrypted = this.decrypt(item.value);
|
||||
return JSON.parse(decrypted);
|
||||
} catch (error) {
|
||||
console.error(`Failed to decrypt item "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return item.value;
|
||||
} catch (error) {
|
||||
console.error(`Storage error getting item "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from storage
|
||||
*/
|
||||
removeItem(key: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
storageInstance.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage error removing item "${key}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if item exists and is not expired
|
||||
*/
|
||||
hasItem(key: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
return this.getItem(key, storage) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all items from storage
|
||||
*/
|
||||
clear(storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
storageInstance.clear();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Storage error clearing storage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys from storage with optional prefix filter
|
||||
*/
|
||||
getKeys(prefix?: string, storage: 'local' | 'session' = 'local'): string[] {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const keys: string[] = [];
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key && (!prefix || key.startsWith(prefix))) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
} catch (error) {
|
||||
console.error('Storage error getting keys:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage usage information
|
||||
*/
|
||||
getStorageInfo(storage: 'local' | 'session' = 'local'): {
|
||||
used: number;
|
||||
total: number;
|
||||
available: number;
|
||||
itemCount: number;
|
||||
} {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
// Calculate used space (approximate)
|
||||
let used = 0;
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
const value = storageInstance.getItem(key);
|
||||
used += key.length + (value?.length || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Most browsers have ~5-10MB limit for localStorage
|
||||
const estimated_total = 5 * 1024 * 1024; // 5MB in bytes
|
||||
|
||||
return {
|
||||
used,
|
||||
total: estimated_total,
|
||||
available: estimated_total - used,
|
||||
itemCount: storageInstance.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Storage error getting storage info:', error);
|
||||
return { used: 0, total: 0, available: 0, itemCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean expired items from storage
|
||||
*/
|
||||
cleanExpired(storage: 'local' | 'session' = 'local'): number {
|
||||
let cleanedCount = 0;
|
||||
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
try {
|
||||
const itemStr = storageInstance.getItem(key);
|
||||
if (itemStr) {
|
||||
const item: StorageItem = JSON.parse(itemStr);
|
||||
if (item.expiresAt && Date.now() > item.expiresAt) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't parse the item, it might be corrupted
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
storageInstance.removeItem(key);
|
||||
cleanedCount++;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Storage error cleaning expired items:', error);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup storage to JSON
|
||||
*/
|
||||
backup(storage: 'local' | 'session' = 'local'): string {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const backup: Record<string, any> = {};
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
const value = storageInstance.getItem(key);
|
||||
if (value) {
|
||||
backup[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
storage: storage,
|
||||
data: backup
|
||||
}, null, 2);
|
||||
} catch (error) {
|
||||
console.error('Storage error creating backup:', error);
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore storage from JSON backup
|
||||
*/
|
||||
restore(backupData: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const backup = JSON.parse(backupData);
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
if (backup.data) {
|
||||
Object.entries(backup.data).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
storageInstance.setItem(key, value);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Storage error restoring backup:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Encryption utilities (basic implementation - use proper crypto in production)
|
||||
private encrypt(text: string): string {
|
||||
try {
|
||||
// This is a simple XOR cipher - replace with proper encryption in production
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(
|
||||
text.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)
|
||||
);
|
||||
}
|
||||
return btoa(result);
|
||||
} catch (error) {
|
||||
console.error('Encryption error:', error);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private decrypt(encryptedText: string): string {
|
||||
try {
|
||||
const text = atob(encryptedText);
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(
|
||||
text.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Decryption error:', error);
|
||||
return encryptedText;
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for common operations
|
||||
/**
|
||||
* Store user authentication data
|
||||
*/
|
||||
setAuthData(data: {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
user_data?: any;
|
||||
tenant_id?: string;
|
||||
}): boolean {
|
||||
const success = [
|
||||
this.setItem('access_token', data.access_token, { encrypt: true }),
|
||||
data.refresh_token ? this.setItem('refresh_token', data.refresh_token, { encrypt: true }) : true,
|
||||
data.user_data ? this.setItem('user_data', data.user_data) : true,
|
||||
data.tenant_id ? this.setItem('tenant_id', data.tenant_id) : true,
|
||||
].every(Boolean);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all authentication data
|
||||
*/
|
||||
clearAuthData(): boolean {
|
||||
return [
|
||||
this.removeItem('access_token'),
|
||||
this.removeItem('refresh_token'),
|
||||
this.removeItem('user_data'),
|
||||
this.removeItem('tenant_id'),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store app preferences
|
||||
*/
|
||||
setPreferences(preferences: Record<string, any>): boolean {
|
||||
return this.setItem('app_preferences', preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app preferences
|
||||
*/
|
||||
getPreferences<T = Record<string, any>>(): T | null {
|
||||
return this.getItem<T>('app_preferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store temporary session data with automatic expiration
|
||||
*/
|
||||
setSessionData(key: string, data: any, expiresInMinutes: number = 30): boolean {
|
||||
return this.setItem(key, data, {
|
||||
storage: 'session',
|
||||
expiresIn: expiresInMinutes * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get temporary session data
|
||||
*/
|
||||
getSessionData<T>(key: string): T | null {
|
||||
return this.getItem<T>(key, 'session');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check storage availability
|
||||
*/
|
||||
isStorageAvailable(storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const test = '__storage_test__';
|
||||
storageInstance.setItem(test, test);
|
||||
storageInstance.removeItem(test);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const storageService = new StorageService();
|
||||
|
||||
// Export class for testing or multiple instances
|
||||
export { StorageService };
|
||||
|
||||
// Legacy compatibility functions
|
||||
export const getStorageItem = <T>(key: string): T | null => storageService.getItem<T>(key);
|
||||
export const setStorageItem = <T>(key: string, value: T, options?: StorageOptions): boolean =>
|
||||
storageService.setItem(key, value, options);
|
||||
export const removeStorageItem = (key: string): boolean => storageService.removeItem(key);
|
||||
Reference in New Issue
Block a user