demo seed change 7
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Sustainability API Service
|
||||
* Environmental impact, SDG compliance, and grant reporting
|
||||
* Sustainability API Service - Microservices Architecture
|
||||
* Fetches data from production and inventory services in parallel
|
||||
* Performs client-side aggregation of sustainability metrics
|
||||
*/
|
||||
|
||||
import apiClient from '../client/apiClient';
|
||||
@@ -9,27 +10,406 @@ import type {
|
||||
SustainabilityWidgetData,
|
||||
SDGCompliance,
|
||||
EnvironmentalImpact,
|
||||
GrantReport
|
||||
GrantReport,
|
||||
WasteMetrics,
|
||||
FinancialImpact,
|
||||
AvoidedWaste,
|
||||
GrantReadiness
|
||||
} from '../types/sustainability';
|
||||
|
||||
const BASE_PATH = '/sustainability';
|
||||
// ===== SERVICE-SPECIFIC API CALLS =====
|
||||
|
||||
/**
|
||||
* Get comprehensive sustainability metrics
|
||||
* Production Service: Get production waste metrics
|
||||
*/
|
||||
export async function getProductionWasteMetrics(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}/production/sustainability/waste-metrics${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return await apiClient.get<any>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Production Service: Get production baseline metrics
|
||||
*/
|
||||
export async function getProductionBaseline(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}/production/sustainability/baseline${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return await apiClient.get<any>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Production Service: Get AI impact on waste reduction
|
||||
*/
|
||||
export async function getProductionAIImpact(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}/production/sustainability/ai-impact${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return await apiClient.get<any>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Production Service: Get summary widget data
|
||||
*/
|
||||
export async function getProductionSummary(
|
||||
tenantId: string,
|
||||
days: number = 30
|
||||
): Promise<any> {
|
||||
return await apiClient.get<any>(
|
||||
`/tenants/${tenantId}/production/sustainability/summary?days=${days}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory Service: Get inventory waste metrics
|
||||
*/
|
||||
export async function getInventoryWasteMetrics(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}/inventory/sustainability/waste-metrics${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return await apiClient.get<any>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory Service: Get expiry alerts
|
||||
*/
|
||||
export async function getInventoryExpiryAlerts(
|
||||
tenantId: string,
|
||||
daysAhead: number = 7
|
||||
): Promise<any> {
|
||||
return await apiClient.get<any>(
|
||||
`/tenants/${tenantId}/inventory/sustainability/expiry-alerts?days_ahead=${daysAhead}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory Service: Get waste events
|
||||
*/
|
||||
export async function getInventoryWasteEvents(
|
||||
tenantId: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
reasonCode?: string
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', limit.toString());
|
||||
params.append('offset', offset.toString());
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
if (reasonCode) params.append('reason_code', reasonCode);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}/inventory/sustainability/waste-events?${queryString}`;
|
||||
|
||||
return await apiClient.get<any>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory Service: Get summary widget data
|
||||
*/
|
||||
export async function getInventorySummary(
|
||||
tenantId: string,
|
||||
days: number = 30
|
||||
): Promise<any> {
|
||||
return await apiClient.get<any>(
|
||||
`/tenants/${tenantId}/inventory/sustainability/summary?days=${days}`
|
||||
);
|
||||
}
|
||||
|
||||
// ===== AGGREGATION FUNCTIONS =====
|
||||
|
||||
/**
|
||||
* Environmental Constants for calculations
|
||||
*/
|
||||
const EnvironmentalConstants = {
|
||||
CO2_PER_KG_WASTE: 1.9, // kg CO2e per kg waste
|
||||
WATER_PER_KG: 1500, // liters per kg
|
||||
LAND_USE_PER_KG: 3.4, // m² per kg
|
||||
TREES_PER_TON_CO2: 50,
|
||||
SDG_TARGET_REDUCTION: 0.50, // 50% reduction target
|
||||
EU_BAKERY_BASELINE_WASTE: 0.25 // 25% baseline
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate environmental impact from total waste
|
||||
*/
|
||||
function calculateEnvironmentalImpact(totalWasteKg: number): EnvironmentalImpact {
|
||||
const co2Kg = totalWasteKg * EnvironmentalConstants.CO2_PER_KG_WASTE;
|
||||
const co2Tons = co2Kg / 1000;
|
||||
const waterLiters = totalWasteKg * EnvironmentalConstants.WATER_PER_KG;
|
||||
const landSqMeters = totalWasteKg * EnvironmentalConstants.LAND_USE_PER_KG;
|
||||
|
||||
return {
|
||||
co2_emissions: {
|
||||
kg: Math.round(co2Kg * 100) / 100,
|
||||
tons: Math.round(co2Tons * 1000) / 1000,
|
||||
trees_to_offset: Math.ceil(co2Tons * EnvironmentalConstants.TREES_PER_TON_CO2)
|
||||
},
|
||||
water_footprint: {
|
||||
liters: Math.round(waterLiters),
|
||||
cubic_meters: Math.round(waterLiters / 1000 * 100) / 100
|
||||
},
|
||||
land_use: {
|
||||
square_meters: Math.round(landSqMeters * 100) / 100,
|
||||
hectares: Math.round(landSqMeters / 10000 * 1000) / 1000
|
||||
},
|
||||
human_equivalents: {
|
||||
car_km_equivalent: Math.round(co2Kg / 0.120), // 120g CO2 per km
|
||||
smartphone_charges: Math.round(co2Kg * 1000 / 8), // 8g CO2 per charge
|
||||
showers_equivalent: Math.round(waterLiters / 65), // 65L per shower
|
||||
trees_planted: Math.ceil(co2Tons * EnvironmentalConstants.TREES_PER_TON_CO2)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SDG compliance status
|
||||
*/
|
||||
function calculateSDGCompliance(
|
||||
currentWastePercentage: number,
|
||||
baselineWastePercentage: number
|
||||
): SDGCompliance {
|
||||
const reductionAchieved = baselineWastePercentage > 0
|
||||
? ((baselineWastePercentage - currentWastePercentage) / baselineWastePercentage) * 100
|
||||
: 0;
|
||||
|
||||
const targetReduction = EnvironmentalConstants.SDG_TARGET_REDUCTION * 100; // 50%
|
||||
const progressToTarget = Math.max(0, Math.min(100, (reductionAchieved / targetReduction) * 100));
|
||||
|
||||
let status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline' | 'above_baseline' = 'baseline';
|
||||
let statusLabel = 'Establishing Baseline';
|
||||
|
||||
if (reductionAchieved >= targetReduction) {
|
||||
status = 'sdg_compliant';
|
||||
statusLabel = 'SDG Compliant';
|
||||
} else if (reductionAchieved >= 30) {
|
||||
status = 'on_track';
|
||||
statusLabel = 'On Track';
|
||||
} else if (reductionAchieved >= 10) {
|
||||
status = 'progressing';
|
||||
statusLabel = 'Progressing';
|
||||
} else if (reductionAchieved > 0) {
|
||||
status = 'baseline';
|
||||
statusLabel = 'Improving from Baseline';
|
||||
} else if (reductionAchieved < 0) {
|
||||
status = 'above_baseline';
|
||||
statusLabel = 'Above Baseline';
|
||||
}
|
||||
|
||||
return {
|
||||
sdg_12_3: {
|
||||
baseline_waste_percentage: Math.round(baselineWastePercentage * 100) / 100,
|
||||
current_waste_percentage: Math.round(currentWastePercentage * 100) / 100,
|
||||
reduction_achieved: Math.round(reductionAchieved * 100) / 100,
|
||||
target_reduction: targetReduction,
|
||||
progress_to_target: Math.round(progressToTarget * 100) / 100,
|
||||
status,
|
||||
status_label: statusLabel,
|
||||
target_waste_percentage: baselineWastePercentage * (1 - EnvironmentalConstants.SDG_TARGET_REDUCTION)
|
||||
},
|
||||
baseline_period: 'first_90_days',
|
||||
certification_ready: status === 'sdg_compliant',
|
||||
improvement_areas: status === 'sdg_compliant' ? [] : ['reduce_waste_further']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess grant readiness based on SDG compliance
|
||||
*/
|
||||
function assessGrantReadiness(sdgCompliance: SDGCompliance): GrantReadiness {
|
||||
const reductionAchieved = sdgCompliance.sdg_12_3.reduction_achieved;
|
||||
const isSdgCompliant = sdgCompliance.certification_ready;
|
||||
|
||||
const grantPrograms: Record<string, any> = {
|
||||
life_circular_economy: {
|
||||
eligible: reductionAchieved >= 30,
|
||||
confidence: reductionAchieved >= 40 ? 'high' : reductionAchieved >= 30 ? 'medium' : 'low',
|
||||
requirements_met: reductionAchieved >= 30,
|
||||
funding_eur: 73_000_000,
|
||||
deadline: '2025-09-23',
|
||||
program_type: 'grant'
|
||||
},
|
||||
horizon_europe_cluster_6: {
|
||||
eligible: isSdgCompliant,
|
||||
confidence: isSdgCompliant ? 'high' : 'low',
|
||||
requirements_met: isSdgCompliant,
|
||||
funding_eur: 880_000_000,
|
||||
deadline: 'rolling_2025',
|
||||
program_type: 'grant'
|
||||
},
|
||||
fedima_sustainability_grant: {
|
||||
eligible: reductionAchieved >= 20,
|
||||
confidence: reductionAchieved >= 25 ? 'high' : reductionAchieved >= 20 ? 'medium' : 'low',
|
||||
requirements_met: reductionAchieved >= 20,
|
||||
funding_eur: 20_000,
|
||||
deadline: '2025-06-30',
|
||||
program_type: 'grant',
|
||||
sector_specific: 'bakery'
|
||||
},
|
||||
eit_food_retail: {
|
||||
eligible: reductionAchieved >= 15,
|
||||
confidence: reductionAchieved >= 20 ? 'high' : reductionAchieved >= 15 ? 'medium' : 'low',
|
||||
requirements_met: reductionAchieved >= 15,
|
||||
funding_eur: 45_000,
|
||||
deadline: 'rolling',
|
||||
program_type: 'grant',
|
||||
sector_specific: 'retail'
|
||||
},
|
||||
un_sdg_certified: {
|
||||
eligible: isSdgCompliant,
|
||||
confidence: isSdgCompliant ? 'high' : 'low',
|
||||
requirements_met: isSdgCompliant,
|
||||
funding_eur: 0,
|
||||
deadline: 'ongoing',
|
||||
program_type: 'certification'
|
||||
}
|
||||
};
|
||||
|
||||
const recommendedApplications = Object.entries(grantPrograms)
|
||||
.filter(([_, program]) => program.eligible && program.confidence !== 'low')
|
||||
.map(([name, _]) => name);
|
||||
|
||||
const eligibleCount = Object.values(grantPrograms).filter(p => p.eligible).length;
|
||||
const overallReadiness = (eligibleCount / Object.keys(grantPrograms).length) * 100;
|
||||
|
||||
return {
|
||||
overall_readiness_percentage: Math.round(overallReadiness),
|
||||
grant_programs: grantPrograms,
|
||||
recommended_applications: recommendedApplications,
|
||||
spain_compliance: {
|
||||
law_1_2025: reductionAchieved >= 50,
|
||||
circular_economy_strategy: reductionAchieved >= 30
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ===== MAIN AGGREGATION FUNCTION =====
|
||||
|
||||
/**
|
||||
* Get comprehensive sustainability metrics by aggregating production and inventory data
|
||||
*/
|
||||
export async function getSustainabilityMetrics(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<SustainabilityMetrics> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
try {
|
||||
// Fetch data from both services in parallel
|
||||
const [productionData, inventoryData, productionBaseline, aiImpact] = await Promise.all([
|
||||
getProductionWasteMetrics(tenantId, startDate, endDate),
|
||||
getInventoryWasteMetrics(tenantId, startDate, endDate),
|
||||
getProductionBaseline(tenantId, startDate, endDate),
|
||||
getProductionAIImpact(tenantId, startDate, endDate)
|
||||
]);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/tenants/${tenantId}${BASE_PATH}/metrics${queryString ? `?${queryString}` : ''}`;
|
||||
// Aggregate waste metrics
|
||||
const productionWaste = (productionData.total_production_waste || 0) + (productionData.total_defects || 0);
|
||||
const totalWasteKg = productionWaste + (inventoryData.inventory_waste_kg || 0);
|
||||
|
||||
return await apiClient.get<SustainabilityMetrics>(url);
|
||||
const totalProductionKg = productionData.total_planned || 0;
|
||||
const wastePercentage = totalProductionKg > 0
|
||||
? (totalWasteKg / totalProductionKg) * 100
|
||||
: 0;
|
||||
|
||||
const wasteMetrics: WasteMetrics = {
|
||||
total_waste_kg: Math.round(totalWasteKg * 100) / 100,
|
||||
production_waste_kg: Math.round(productionWaste * 100) / 100,
|
||||
expired_waste_kg: Math.round((inventoryData.inventory_waste_kg || 0) * 100) / 100,
|
||||
waste_percentage: Math.round(wastePercentage * 100) / 100,
|
||||
waste_by_reason: {
|
||||
...(productionData.waste_by_defect_type || {}),
|
||||
...(inventoryData.waste_by_reason || {})
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate environmental impact
|
||||
const environmentalImpact = calculateEnvironmentalImpact(totalWasteKg);
|
||||
|
||||
// Calculate SDG compliance
|
||||
const baselineWastePercentage = productionBaseline.waste_percentage ||
|
||||
EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100;
|
||||
const sdgCompliance = calculateSDGCompliance(wastePercentage, baselineWastePercentage);
|
||||
|
||||
// Calculate avoided waste from AI
|
||||
const wasteAvoidedKg = aiImpact.impact?.waste_avoided_kg || 0;
|
||||
const avoidedWaste: AvoidedWaste = {
|
||||
waste_avoided_kg: Math.round(wasteAvoidedKg * 100) / 100,
|
||||
ai_assisted_batches: aiImpact.ai_batches?.count || 0,
|
||||
environmental_impact_avoided: {
|
||||
co2_kg: Math.round(wasteAvoidedKg * EnvironmentalConstants.CO2_PER_KG_WASTE * 100) / 100,
|
||||
water_liters: Math.round(wasteAvoidedKg * EnvironmentalConstants.WATER_PER_KG)
|
||||
},
|
||||
methodology: 'ai_vs_manual_comparison'
|
||||
};
|
||||
|
||||
// Calculate financial impact
|
||||
const inventoryCost = inventoryData.waste_cost_eur || 0;
|
||||
const productionCost = productionWaste * 3.50; // €3.50/kg avg
|
||||
const totalCost = inventoryCost + productionCost;
|
||||
|
||||
const financialImpact: FinancialImpact = {
|
||||
waste_cost_eur: Math.round(totalCost * 100) / 100,
|
||||
cost_per_kg: 3.50,
|
||||
potential_monthly_savings: Math.round((aiImpact.impact?.cost_savings_eur || 0) * 100) / 100,
|
||||
annual_projection: Math.round((aiImpact.impact?.cost_savings_eur || 0) * 12 * 100) / 100
|
||||
};
|
||||
|
||||
// Assess grant readiness
|
||||
const grantReadiness = assessGrantReadiness(sdgCompliance);
|
||||
|
||||
return {
|
||||
period: productionData.period || {
|
||||
start_date: startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
end_date: endDate || new Date().toISOString(),
|
||||
days: 30
|
||||
},
|
||||
waste_metrics: wasteMetrics,
|
||||
environmental_impact: environmentalImpact,
|
||||
sdg_compliance: sdgCompliance,
|
||||
avoided_waste: avoidedWaste,
|
||||
financial_impact: financialImpact,
|
||||
grant_readiness: grantReadiness
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error aggregating sustainability metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,18 +419,57 @@ export async function getSustainabilityWidgetData(
|
||||
tenantId: string,
|
||||
days: number = 30
|
||||
): Promise<SustainabilityWidgetData> {
|
||||
return await apiClient.get<SustainabilityWidgetData>(
|
||||
`/tenants/${tenantId}${BASE_PATH}/widget?days=${days}`
|
||||
);
|
||||
try {
|
||||
// Fetch summaries from both services in parallel
|
||||
const [productionSummary, inventorySummary] = await Promise.all([
|
||||
getProductionSummary(tenantId, days),
|
||||
getInventorySummary(tenantId, days)
|
||||
]);
|
||||
|
||||
const productionWasteWidget = (productionSummary.total_production_waste || 0) + (productionSummary.total_defects || 0);
|
||||
const totalWasteKg = productionWasteWidget + (inventorySummary.inventory_waste_kg || 0);
|
||||
|
||||
const totalProduction = productionSummary.total_planned || productionSummary.total_production_kg || 0;
|
||||
const wastePercentage = totalProduction > 0 ? ((totalWasteKg / totalProduction) * 100) : 0;
|
||||
|
||||
const baselinePercentage = productionSummary.waste_percentage || 25;
|
||||
const reductionPercentage = baselinePercentage > 0
|
||||
? ((baselinePercentage - wastePercentage) / baselinePercentage) * 100
|
||||
: 0;
|
||||
|
||||
const co2SavedKg = totalWasteKg * EnvironmentalConstants.CO2_PER_KG_WASTE;
|
||||
const waterSavedLiters = totalWasteKg * EnvironmentalConstants.WATER_PER_KG;
|
||||
|
||||
return {
|
||||
total_waste_kg: Math.round(totalWasteKg * 100) / 100,
|
||||
waste_reduction_percentage: Math.round(reductionPercentage * 100) / 100,
|
||||
co2_saved_kg: Math.round(co2SavedKg * 100) / 100,
|
||||
water_saved_liters: Math.round(waterSavedLiters),
|
||||
trees_equivalent: Math.ceil((co2SavedKg / 1000) * EnvironmentalConstants.TREES_PER_TON_CO2),
|
||||
sdg_status: reductionPercentage >= 50 ? 'sdg_compliant' :
|
||||
reductionPercentage >= 37.5 ? 'on_track' :
|
||||
reductionPercentage >= 12.5 ? 'progressing' : 'baseline',
|
||||
sdg_progress: Math.min(100, (reductionPercentage / 50) * 100),
|
||||
grant_programs_ready: reductionPercentage >= 50 ? 5 :
|
||||
reductionPercentage >= 30 ? 3 :
|
||||
reductionPercentage >= 15 ? 2 : 0,
|
||||
financial_savings_eur: Math.round(
|
||||
((inventorySummary.waste_cost_eur || 0) + (productionWasteWidget * 3.50)) * 100
|
||||
) / 100
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching sustainability widget data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SDG 12.3 compliance status
|
||||
*/
|
||||
export async function getSDGCompliance(tenantId: string): Promise<SDGCompliance> {
|
||||
return await apiClient.get<SDGCompliance>(
|
||||
`/tenants/${tenantId}${BASE_PATH}/sdg-compliance`
|
||||
);
|
||||
const metrics = await getSustainabilityMetrics(tenantId);
|
||||
return metrics.sdg_compliance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,13 +479,16 @@ export async function getEnvironmentalImpact(
|
||||
tenantId: string,
|
||||
days: number = 30
|
||||
): Promise<EnvironmentalImpact> {
|
||||
return await apiClient.get<EnvironmentalImpact>(
|
||||
`/tenants/${tenantId}${BASE_PATH}/environmental-impact?days=${days}`
|
||||
);
|
||||
const endDate = new Date().toISOString();
|
||||
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const metrics = await getSustainabilityMetrics(tenantId, startDate, endDate);
|
||||
return metrics.environmental_impact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export grant application report
|
||||
* Note: This still uses the aggregated metrics approach
|
||||
*/
|
||||
export async function exportGrantReport(
|
||||
tenantId: string,
|
||||
@@ -74,12 +496,35 @@ export async function exportGrantReport(
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<GrantReport> {
|
||||
const payload: any = { grant_type: grantType, format: 'json' };
|
||||
if (startDate) payload.start_date = startDate;
|
||||
if (endDate) payload.end_date = endDate;
|
||||
const metrics = await getSustainabilityMetrics(tenantId, startDate, endDate);
|
||||
|
||||
return await apiClient.post<GrantReport>(
|
||||
`/tenants/${tenantId}${BASE_PATH}/export/grant-report`,
|
||||
payload
|
||||
);
|
||||
return {
|
||||
report_metadata: {
|
||||
generated_at: new Date().toISOString(),
|
||||
report_type: grantType,
|
||||
period: metrics.period,
|
||||
tenant_id: tenantId
|
||||
},
|
||||
executive_summary: {
|
||||
total_waste_reduced_kg: metrics.avoided_waste.waste_avoided_kg,
|
||||
waste_reduction_percentage: metrics.sdg_compliance.sdg_12_3.reduction_achieved,
|
||||
co2_emissions_avoided_kg: metrics.avoided_waste.environmental_impact_avoided.co2_kg,
|
||||
financial_savings_eur: metrics.financial_impact.potential_monthly_savings,
|
||||
sdg_compliance_status: metrics.sdg_compliance.sdg_12_3.status_label
|
||||
},
|
||||
detailed_metrics: metrics,
|
||||
certifications: {
|
||||
sdg_12_3_compliant: metrics.sdg_compliance.certification_ready,
|
||||
grant_programs_eligible: metrics.grant_readiness.recommended_applications
|
||||
},
|
||||
supporting_data: {
|
||||
baseline_comparison: {
|
||||
baseline: metrics.sdg_compliance.sdg_12_3.baseline_waste_percentage,
|
||||
current: metrics.sdg_compliance.sdg_12_3.current_waste_percentage,
|
||||
improvement: metrics.sdg_compliance.sdg_12_3.reduction_achieved
|
||||
},
|
||||
environmental_benefits: metrics.environmental_impact,
|
||||
financial_benefits: metrics.financial_impact
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface SDG123Metrics {
|
||||
reduction_achieved: number;
|
||||
target_reduction: number;
|
||||
progress_to_target: number;
|
||||
status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline';
|
||||
status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline' | 'above_baseline';
|
||||
status_label: string;
|
||||
target_waste_percentage: number;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { Button, Card, CardHeader, CardBody, CardFooter, CardTitle, CardDescription, Badge, Alert, AlertDescription, Modal, ModalHeader, ModalBody, ModalFooter } from '../../components/ui';
|
||||
import { apiClient } from '../../api/client';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../../components/ui/Card';
|
||||
import { Badge } from '../../components/ui/Badge';
|
||||
import { Alert, AlertDescription } from '../../components/ui/Alert';
|
||||
import Modal, { ModalHeader, ModalBody, ModalFooter } from '../../components/ui/Modal/Modal';
|
||||
import { PublicLayout } from '../../components/layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -17,7 +13,6 @@ import {
|
||||
Building,
|
||||
Package,
|
||||
BarChart3,
|
||||
|
||||
ChefHat,
|
||||
CreditCard,
|
||||
Bell,
|
||||
@@ -40,7 +35,8 @@ import {
|
||||
ShoppingBasket as ShoppingBasketIcon,
|
||||
TrendingUp as ChartIcon,
|
||||
DollarSign as MoneyIcon,
|
||||
ArrowRight
|
||||
ArrowRight,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
|
||||
const DemoPage = () => {
|
||||
@@ -117,7 +113,8 @@ const DemoPage = () => {
|
||||
},
|
||||
accountType: 'professional',
|
||||
baseTenantId: 'a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6',
|
||||
color: 'blue'
|
||||
color: 'primary',
|
||||
gradient: 'from-amber-500 to-orange-600'
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
@@ -146,7 +143,8 @@ const DemoPage = () => {
|
||||
},
|
||||
accountType: 'enterprise',
|
||||
baseTenantId: 'c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8',
|
||||
color: 'purple'
|
||||
color: 'secondary',
|
||||
gradient: 'from-emerald-500 to-teal-600'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -583,16 +581,6 @@ const DemoPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getIconColor = (color) => {
|
||||
const colors = {
|
||||
blue: 'text-blue-600',
|
||||
purple: 'text-purple-600',
|
||||
green: 'text-green-600',
|
||||
orange: 'text-orange-600'
|
||||
};
|
||||
return colors[color] || 'text-blue-600';
|
||||
};
|
||||
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="default"
|
||||
@@ -604,126 +592,211 @@ const DemoPage = () => {
|
||||
variant: "default"
|
||||
}}
|
||||
>
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-[var(--text-primary)] mb-4">
|
||||
Prueba Nuestra Plataforma
|
||||
{/* Hero Section with Enhanced Design */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 dark:from-[var(--bg-primary)] dark:via-[var(--bg-secondary)] dark:to-[var(--color-primary)]/10 py-24">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 bg-pattern opacity-50"></div>
|
||||
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute top-20 left-10 w-72 h-72 bg-[var(--color-primary)]/10 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div className="absolute bottom-10 right-10 w-96 h-96 bg-[var(--color-secondary)]/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center space-y-6 animate-fade-in">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--color-primary)]/10 dark:bg-[var(--color-primary)]/20 border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/30 mb-4">
|
||||
<Sparkles className="w-4 h-4 text-[var(--color-primary)]" />
|
||||
<span className="text-sm font-medium text-[var(--color-primary)]">Experiencia Demo Gratuita</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-[var(--text-primary)] mb-6 leading-tight">
|
||||
Prueba Nuestra
|
||||
<span className="block bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent">
|
||||
Plataforma de Gestión
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||
Elige tu experiencia de demostración ideal y explora cómo nuestra
|
||||
plataforma puede transformar tu negocio de panadería
|
||||
|
||||
<p className="text-xl md:text-2xl text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
|
||||
Elige tu experiencia de demostración ideal y descubre cómo nuestra plataforma puede transformar tu negocio de panadería
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-8 pt-4 text-sm text-[var(--text-tertiary)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span>Sin tarjeta de crédito</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span>Configuración instantánea</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span>Datos reales de ejemplo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<section className="py-16 bg-[var(--bg-primary)]">
|
||||
<section className="py-20 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Demo Options */}
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
||||
{demoOptions.map((option) => (
|
||||
<Card
|
||||
{/* Demo Options with Improved Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
{demoOptions.map((option, index) => (
|
||||
<div
|
||||
key={option.id}
|
||||
className={`cursor-pointer hover:shadow-xl transition-all border-2 ${selectedTier === option.id ? 'border-primary bg-primary/5' : 'border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
className={`
|
||||
bg-[var(--bg-primary)]
|
||||
border border-[var(--border-primary)]
|
||||
rounded-xl
|
||||
shadow-lg
|
||||
overflow-hidden
|
||||
transition-all duration-300
|
||||
hover:shadow-2xl
|
||||
cursor-pointer
|
||||
${selectedTier === option.id
|
||||
? 'ring-2 ring-[var(--color-primary)] shadow-xl'
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedTier(option.id)}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<option.icon className={`w-12 h-12 ${getIconColor(option.color)} text-[var(--color-primary)]`} />
|
||||
<div>
|
||||
<CardTitle className="text-2xl text-[var(--text-primary)]">{option.title}</CardTitle>
|
||||
<Badge
|
||||
variant={option.tier === 'enterprise' ? 'premium' : 'default'}
|
||||
className="mt-2 capitalize"
|
||||
>
|
||||
{option.subtitle}
|
||||
</Badge>
|
||||
{/* Card Header with Gradient */}
|
||||
<div className={`bg-gradient-to-r ${option.gradient} p-6`}>
|
||||
<div className="flex items-start justify-between w-full text-white mb-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div className="p-3 bg-white/20 backdrop-blur-sm rounded-xl">
|
||||
<option.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-white mb-2">
|
||||
{option.title}
|
||||
</h3>
|
||||
<Badge
|
||||
variant={option.tier === 'enterprise' ? 'secondary' : 'default'}
|
||||
className="bg-white/20 backdrop-blur-sm text-white border-white/30 capitalize font-semibold"
|
||||
>
|
||||
{option.subtitle}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{selectedTier === option.id && (
|
||||
<div className="animate-scale-in">
|
||||
<CheckCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white/90 text-base leading-relaxed">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card Body */}
|
||||
<div className="p-6">
|
||||
{/* Features List with Icons */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] text-sm uppercase tracking-wide mb-4">
|
||||
Características Incluidas
|
||||
</h4>
|
||||
{option.features.slice(0, 6).map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3 group">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className="p-1 rounded-full bg-[var(--color-success)]/10 group-hover:bg-[var(--color-success)]/20 transition-colors">
|
||||
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-secondary)] group-hover:text-[var(--text-primary)] transition-colors">
|
||||
{feature}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{option.features.length > 6 && (
|
||||
<div className="flex items-start gap-3 pt-2">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className="p-1 rounded-full bg-[var(--color-info)]/10">
|
||||
<PlusCircle className="w-4 h-4 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[var(--color-info)]">
|
||||
+ {option.features.length - 6} funciones más
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Characteristics Grid with Enhanced Design */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-xl bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-tertiary)] border border-[var(--border-primary)]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide">Ubicaciones</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{option.characteristics.locations}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide">Empleados</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{option.characteristics.employees}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
|
||||
<Factory className="w-4 h-4" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide">Producción</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{option.characteristics.productionModel}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide">Canales</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{option.characteristics.salesChannels}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)] mt-4">{option.description}</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{/* Features List */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{option.features.slice(0, 6).map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
{option.features.length > 6 && (
|
||||
<div className="flex items-start gap-3 text-sm text-[var(--text-tertiary)]">
|
||||
<PlusCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<span>+ {option.features.length - 6} funciones más</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Card Footer */}
|
||||
<div className="p-6 bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStartDemo(option.accountType, option.tier);
|
||||
}}
|
||||
disabled={creatingTier !== null}
|
||||
size="lg"
|
||||
isFullWidth={true}
|
||||
variant={option.tier === 'enterprise' ? 'gradient' : 'primary'}
|
||||
className="font-semibold group"
|
||||
>
|
||||
{creatingTier === option.tier ? (
|
||||
<span className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white/30 border-t-white" />
|
||||
<span>{getLoadingMessage(option.tier, cloneProgress.overall)}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Zap className="w-5 h-5" />
|
||||
<span>Iniciar Demo {option.tier === 'enterprise' ? 'Enterprise' : 'Professional'}</span>
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Characteristics Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm border-t border-[var(--border-primary)] pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<div>
|
||||
<span className="font-semibold text-[var(--text-primary)]">Ubicaciones:</span>
|
||||
<p className="text-[var(--text-secondary)]">{option.characteristics.locations}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<div>
|
||||
<span className="font-semibold text-[var(--text-primary)]">Empleados:</span>
|
||||
<p className="text-[var(--text-secondary)]">{option.characteristics.employees}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Factory className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<div>
|
||||
<span className="font-semibold text-[var(--text-primary)]">Producción:</span>
|
||||
<p className="text-[var(--text-secondary)]">{option.characteristics.productionModel}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<div>
|
||||
<span className="font-semibold text-[var(--text-primary)]">Canales:</span>
|
||||
<p className="text-[var(--text-secondary)]">{option.characteristics.salesChannels}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => handleStartDemo(option.accountType, option.tier)}
|
||||
disabled={creatingTier !== null}
|
||||
className="w-full h-12 text-lg font-semibold"
|
||||
variant={option.tier === 'enterprise' ? 'premium' : 'default'}
|
||||
>
|
||||
{creatingTier === option.tier ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
{getLoadingMessage(option.tier, cloneProgress.overall)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>Iniciar Demo {option.tier === 'enterprise' ? 'Enterprise' : 'Professional'}</span>
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Loading Progress Modal */}
|
||||
{/* Loading Progress Modal with Enhanced Design */}
|
||||
{creatingTier !== null && (
|
||||
<Modal
|
||||
isOpen={creatingTier !== null}
|
||||
@@ -733,95 +806,129 @@ const DemoPage = () => {
|
||||
<ModalHeader
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary"></div>
|
||||
<span>Configurando Tu Demo</span>
|
||||
<div className="relative">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--color-primary)]/30 border-t-[var(--color-primary)]"></div>
|
||||
<div className="absolute inset-0 rounded-full bg-[var(--color-primary)]/10 animate-pulse"></div>
|
||||
</div>
|
||||
<span className="text-xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent">
|
||||
Configurando Tu Demo
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
showCloseButton={false}
|
||||
/>
|
||||
<ModalBody padding="lg">
|
||||
<div className="space-y-6">
|
||||
{/* Overall Progress Section */}
|
||||
<div className="text-center">
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="font-medium">Progreso Total</span>
|
||||
<span className="font-semibold text-lg">{cloneProgress.overall}%</span>
|
||||
<ModalBody padding="xl">
|
||||
<div className="space-y-8">
|
||||
{/* Overall Progress Section with Enhanced Visual */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="flex justify-between items-baseline mb-3">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Progreso Total</span>
|
||||
<span className="text-3xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent">
|
||||
{cloneProgress.overall}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
||||
|
||||
<div className="relative w-full bg-[var(--bg-tertiary)] rounded-full h-4 overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-600 h-3 rounded-full transition-all duration-500 relative overflow-hidden"
|
||||
className="relative bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-primary-light)] to-[var(--color-secondary)] h-4 rounded-full transition-all duration-500 ease-out shadow-lg"
|
||||
style={{ width: `${cloneProgress.overall}%` }}
|
||||
>
|
||||
{/* Shimmer Effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent animate-shimmer"></div>
|
||||
{/* Glow Effect */}
|
||||
<div className="absolute inset-0 rounded-full shadow-[0_0_20px_rgba(217,119,6,0.5)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{estimatedRemainingSeconds !== null && estimatedRemainingSeconds > 0 && (
|
||||
<div className="mt-3 text-sm text-[var(--text-secondary)]">
|
||||
~{estimatedRemainingSeconds}s restantes
|
||||
<div className="flex items-center justify-center gap-2 mt-4">
|
||||
<Clock className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
Aproximadamente <span className="font-semibold text-[var(--color-primary)]">{estimatedRemainingSeconds}s</span> restantes
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-[var(--text-secondary)]">
|
||||
<p className="text-base text-[var(--text-secondary)] font-medium mt-4">
|
||||
{getLoadingMessage(creatingTier, cloneProgress.overall)}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Detailed Progress */}
|
||||
{/* Enterprise Detailed Progress with Enhanced Visuals */}
|
||||
{creatingTier === 'enterprise' && (
|
||||
<div className="space-y-5 mt-6">
|
||||
<div className="space-y-5 mt-8">
|
||||
{/* Parent Tenant */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-4 border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
<span className="font-semibold text-blue-900 dark:text-blue-100">Obrador Central</span>
|
||||
<div className="relative overflow-hidden rounded-2xl p-5 bg-gradient-to-br from-[var(--color-info)]/10 via-[var(--color-info)]/5 to-transparent dark:from-[var(--color-info)]/20 dark:via-[var(--color-info)]/10 border border-[var(--color-info)]/30 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] shadow-lg animate-pulse"></div>
|
||||
<span className="font-bold text-[var(--color-info-dark)] dark:text-[var(--color-info-light)] text-lg">
|
||||
Obrador Central
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium text-blue-700 dark:text-blue-300">{cloneProgress.parent}%</span>
|
||||
<span className="font-bold text-xl text-[var(--color-info)] dark:text-[var(--color-info-light)]">
|
||||
{cloneProgress.parent}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
|
||||
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-400 to-blue-600 h-2.5 rounded-full transition-all duration-500"
|
||||
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] h-3 rounded-full transition-all duration-500 shadow-lg relative overflow-hidden"
|
||||
style={{ width: `${cloneProgress.parent}%` }}
|
||||
></div>
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Child Outlets */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{cloneProgress.children.map((progress, index) => (
|
||||
{/* Child Outlets with Grid Layout */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{['Barcelona', 'Valencia', 'Bilbao'].map((city, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-green-50 dark:bg-green-900/20 rounded-lg p-3 border border-green-200 dark:border-green-800"
|
||||
className="relative overflow-hidden rounded-xl p-4 bg-gradient-to-br from-[var(--color-success)]/10 via-[var(--color-success)]/5 to-transparent dark:from-[var(--color-success)]/20 dark:via-[var(--color-success)]/10 border border-[var(--color-success)]/30 shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs font-medium text-green-700 dark:text-green-300">Outlet {index + 1}</span>
|
||||
<span className="text-xs font-semibold text-green-700 dark:text-green-300">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-gradient-to-r from-green-400 to-green-600 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs font-bold text-[var(--color-success-dark)] dark:text-[var(--color-success-light)] uppercase tracking-wide">
|
||||
{city}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-[var(--color-success-dark)] dark:text-[var(--color-success-light)]">
|
||||
{cloneProgress.children[index]}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2.5 overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[var(--color-success)] to-[var(--color-success-dark)] h-2.5 rounded-full transition-all duration-500 relative overflow-hidden"
|
||||
style={{ width: `${cloneProgress.children[index]}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Distribution System */}
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 rounded-xl p-4 border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-purple-500"></div>
|
||||
<span className="font-semibold text-purple-900 dark:text-purple-100">Distribución</span>
|
||||
<div className="relative overflow-hidden rounded-2xl p-5 bg-gradient-to-br from-[var(--color-secondary)]/10 via-[var(--color-secondary)]/5 to-transparent dark:from-[var(--color-secondary)]/20 dark:via-[var(--color-secondary)]/10 border border-[var(--color-secondary)]/30 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Truck className="w-5 h-5 text-[var(--color-secondary)] animate-bounce" />
|
||||
<span className="font-bold text-[var(--color-secondary-dark)] dark:text-[var(--color-secondary-light)] text-lg">
|
||||
Sistema de Distribución
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium text-purple-700 dark:text-purple-300">{cloneProgress.distribution}%</span>
|
||||
<span className="font-bold text-xl text-[var(--color-secondary)] dark:text-[var(--color-secondary-light)]">
|
||||
{cloneProgress.distribution}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
|
||||
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="bg-gradient-to-r from-purple-400 to-purple-600 h-2.5 rounded-full transition-all duration-500"
|
||||
className="bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] h-3 rounded-full transition-all duration-500 shadow-lg relative overflow-hidden"
|
||||
style={{ width: `${cloneProgress.distribution}%` }}
|
||||
></div>
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -829,36 +936,44 @@ const DemoPage = () => {
|
||||
|
||||
{/* Professional Progress Indicator */}
|
||||
{creatingTier === 'professional' && cloneProgress.overall < 100 && (
|
||||
<div className="text-center py-3">
|
||||
<div className="flex justify-center items-center gap-1">
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||
<div className="text-center py-6">
|
||||
<div className="flex justify-center items-center gap-2 mb-4">
|
||||
<div className="w-3 h-3 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||
<div className="w-3 h-3 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||
<div className="w-3 h-3 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-tertiary)] mt-2">
|
||||
<p className="text-sm text-[var(--text-tertiary)] font-medium">
|
||||
Procesando servicios en paralelo...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Information Box */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs text-[var(--text-tertiary)] text-center">
|
||||
{creatingTier === 'enterprise'
|
||||
? 'Creando obrador central, outlets y sistema de distribución...'
|
||||
: 'Personalizando tu panadería con datos reales...'}
|
||||
</p>
|
||||
{/* Information Box with Enhanced Design */}
|
||||
<div className="mt-6 rounded-xl p-4 bg-gradient-to-r from-[var(--bg-secondary)] to-[var(--bg-tertiary)] border border-[var(--border-primary)] shadow-inner">
|
||||
<div className="flex items-start gap-3">
|
||||
<Activity className="w-5 h-5 text-[var(--color-info)] flex-shrink-0 mt-0.5 animate-pulse" />
|
||||
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">
|
||||
{creatingTier === 'enterprise'
|
||||
? 'Creando obrador central, outlets y sistema de distribución con datos reales de ejemplo...'
|
||||
: 'Personalizando tu panadería con inventario, recetas, y datos de ventas realistas...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Error Alert */}
|
||||
{/* Error Alert with Enhanced Design */}
|
||||
{creationError && (
|
||||
<Alert variant="destructive" className="max-w-md mx-auto mt-4">
|
||||
<AlertDescription>{creationError}</AlertDescription>
|
||||
</Alert>
|
||||
<div className="max-w-md mx-auto mt-6 animate-shake">
|
||||
<Alert variant="destructive" className="shadow-lg border-2">
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<span className="flex-shrink-0 w-2 h-2 bg-[var(--color-error)] rounded-full animate-pulse"></span>
|
||||
{creationError}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Partial Status Warning Modal */}
|
||||
@@ -953,29 +1068,29 @@ const DemoPage = () => {
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-blue-900 dark:text-blue-100">
|
||||
<div className="bg-[var(--color-info)]/10 dark:bg-[var(--color-info)]/20 p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-[var(--color-info-dark)] dark:text-[var(--color-info-light)]">
|
||||
1. Seguir Esperando
|
||||
</h4>
|
||||
<p className="text-xs text-blue-800 dark:text-blue-200 mt-1">
|
||||
<p className="text-xs text-[var(--color-info)] dark:text-[var(--color-info-light)] mt-1">
|
||||
La sesión puede completarse en cualquier momento. Mantén esta página abierta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-green-900 dark:text-green-100">
|
||||
<div className="bg-[var(--color-success)]/10 dark:bg-[var(--color-success)]/20 p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-[var(--color-success-dark)] dark:text-[var(--color-success-light)]">
|
||||
2. Iniciar con Datos Parciales
|
||||
</h4>
|
||||
<p className="text-xs text-green-800 dark:text-green-200 mt-1">
|
||||
<p className="text-xs text-[var(--color-success)] dark:text-[var(--color-success-light)] mt-1">
|
||||
Accede a la demo ahora con los servicios que ya estén listos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="bg-[var(--bg-secondary)] dark:bg-[var(--bg-tertiary)] p-3 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-[var(--text-primary)] dark:text-[var(--text-primary)]">
|
||||
3. Cancelar e Intentar de Nuevo
|
||||
</h4>
|
||||
<p className="text-xs text-gray-800 dark:text-gray-200 mt-1">
|
||||
<p className="text-xs text-[var(--text-secondary)] dark:text-[var(--text-secondary)] mt-1">
|
||||
Cancela esta sesión y crea una nueva desde cero.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1024,4 +1139,4 @@ const DemoPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoPage;
|
||||
export default DemoPage;
|
||||
|
||||
Reference in New Issue
Block a user