Improve the design of the frontend

This commit is contained in:
Urtzi Alfaro
2025-08-08 19:21:23 +02:00
parent 488bb3ef93
commit 62ca49d4b8
53 changed files with 5395 additions and 5387 deletions

View File

@@ -1,252 +0,0 @@
# Frontend API Alignment Analysis Report
## Executive Summary
The frontend API abstraction layer has been thoroughly analyzed and tested against the backend services. The results show a **62.5% success rate** with **5 out of 8 tests passing**. The frontend API structure is well-designed and mostly aligned with backend expectations, but there are some specific areas that need attention.
## ✅ What Works Well
### 1. Authentication Service (`AuthService`)
- **Perfect Alignment**: Registration and login endpoints work flawlessly
- **Response Structure**: Backend response matches frontend expectations exactly
- **Token Handling**: Access token, refresh token, and user object are properly structured
- **Type Safety**: Frontend types match backend schemas
```typescript
// Frontend expectation matches backend reality
interface LoginResponse {
access_token: string;
refresh_token?: string;
token_type: string;
expires_in: number;
user?: UserData;
}
```
### 2. Tenant Service (`TenantService`)
- **Excellent Alignment**: Tenant creation works perfectly through `/tenants/register`
- **Response Structure**: All expected fields present (`id`, `name`, `owner_id`, `is_active`, `created_at`)
- **Additional Fields**: Backend provides extra useful fields (`subdomain`, `business_type`, `subscription_tier`)
### 3. Data Service - Validation (`DataService.validateSalesData`)
- **Perfect Validation**: Data validation endpoint works correctly
- **Rich Response**: Provides comprehensive validation information including file size, processing estimates, and suggestions
- **Error Handling**: Proper validation result structure with errors, warnings, and summary
## ⚠️ Issues Found & Recommendations
### 1. Data Service - Import Endpoint Mismatch
**Issue**: The frontend `uploadSalesHistory()` method is calling the validation endpoint instead of the actual import endpoint.
**Current Frontend Code**:
```typescript
async uploadSalesHistory(tenantId: string, data, additionalData = {}) {
// This calls validation endpoint, not import
return this.apiClient.post(`/tenants/${tenantId}/sales/import/validate-json`, requestData);
}
```
**Backend Reality**:
- Validation: `/tenants/{tenant_id}/sales/import/validate-json`
- Actual Import: `/tenants/{tenant_id}/sales/import` ❌ (not being called)
**Recommendation**: Fix the frontend service to call the correct import endpoint:
```typescript
async uploadSalesHistory(tenantId: string, file: File, additionalData = {}) {
return this.apiClient.upload(`/tenants/${tenantId}/sales/import`, file, additionalData);
}
```
### 2. Training Service - Status Endpoint Issue
**Issue**: Training job status endpoint returns 404 "Training job not found"
**Analysis**:
- Job creation works: ✅ `/tenants/{tenant_id}/training/jobs`
- Job status fails: ❌ `/tenants/{tenant_id}/training/jobs/{job_id}/status`
**Likely Cause**: There might be a timing issue where the job isn't immediately available for status queries, or the endpoint path differs from frontend expectations.
**Recommendation**:
1. Add retry logic with exponential backoff for status checks
2. Verify the exact backend endpoint path in the training service
3. Consider using WebSocket for real-time status updates instead
### 3. Data Service - Products List Empty
**Issue**: Products list returns empty array even after data upload
**Analysis**:
- Data validation shows 3,655 records ✅
- Products endpoint returns `[]`
**Likely Cause**: The data wasn't actually imported (see Issue #1), so no products are available in the database.
**Recommendation**: Fix the import endpoint first, then products should be available.
### 4. Forecasting Service - Missing Required Fields
**Issue**: Forecast creation fails due to missing required `location` field
**Frontend Request**:
```javascript
{
"product_name": "pan",
"forecast_date": "2025-08-08",
"forecast_days": 7,
"confidence_level": 0.85
}
```
**Backend Expectation**:
```python
# Missing required field: location
class ForecastRequest(BaseModel):
product_name: str
location: LocationData # Required but missing
# ... other fields
```
**Recommendation**: Update frontend forecasting service to include location data:
```typescript
async createForecast(tenantId: string, request: ForecastRequest) {
const forecastData = {
...request,
location: {
latitude: 40.4168, // Get from tenant data
longitude: -3.7038
}
};
return this.apiClient.post(`/tenants/${tenantId}/forecasts/single`, forecastData);
}
```
## 📋 Frontend API Improvements Needed
### 1. **Data Service Import Method**
```typescript
// Fix the uploadSalesHistory method
async uploadSalesHistory(tenantId: string, file: File, additionalData = {}) {
return this.apiClient.upload(`/tenants/${tenantId}/sales/import`, file, {
file_format: this.detectFileFormat(file),
source: 'onboarding_upload',
...additionalData
});
}
```
### 2. **Training Service Status Polling**
```typescript
async waitForTrainingCompletion(tenantId: string, jobId: string, maxAttempts = 30) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const status = await this.getTrainingJobStatus(tenantId, jobId);
if (status.status === 'completed' || status.status === 'failed') {
return status;
}
await this.sleep(5000); // Wait 5 seconds
} catch (error) {
if (attempt < 3) continue; // Retry first few attempts
throw error;
}
}
throw new Error('Training status timeout');
}
```
### 3. **Forecasting Service Location Support**
```typescript
async createForecast(tenantId: string, request: ForecastRequest) {
// Get tenant location or use default
const tenant = await this.tenantService.getTenant(tenantId);
const location = tenant.location || { latitude: 40.4168, longitude: -3.7038 };
return this.apiClient.post(`/tenants/${tenantId}/forecasts/single`, {
...request,
location
});
}
```
### 4. **Enhanced Error Handling**
```typescript
// Add response transformation middleware
class ApiResponseTransformer {
static transform<T>(response: any, expectedFields: string[]): T {
const missing = expectedFields.filter(field => !(field in response));
if (missing.length > 0) {
console.warn(`Missing expected fields: ${missing.join(', ')}`);
}
return response;
}
}
```
## 🎯 Backend API Alignment Score
| Service | Endpoint | Status | Score | Notes |
|---------|----------|--------|--------|-------|
| **Auth** | Registration | ✅ | 100% | Perfect alignment |
| **Auth** | Login | ✅ | 100% | Perfect alignment |
| **Tenant** | Create | ✅ | 100% | Perfect alignment |
| **Data** | Validation | ✅ | 100% | Perfect alignment |
| **Data** | Import | ⚠️ | 50% | Wrong endpoint called |
| **Data** | Products List | ⚠️ | 50% | Empty due to import issue |
| **Training** | Job Start | ✅ | 100% | Perfect alignment |
| **Training** | Job Status | ❌ | 0% | 404 error |
| **Forecasting** | Create | ❌ | 25% | Missing required fields |
**Overall Score: 62.5%** - Good foundation with specific issues to address
## 🚀 Action Items
### High Priority
1. **Fix Data Import Endpoint** - Update frontend to call actual import endpoint
2. **Add Location Support to Forecasting** - Include required location field
3. **Investigate Training Status 404** - Debug timing or endpoint path issues
### Medium Priority
1. **Add Response Transformation Layer** - Handle different response formats gracefully
2. **Implement Status Polling** - Add retry logic for async operations
3. **Enhanced Error Handling** - Better error messages and fallback strategies
### Low Priority
1. **Add Request/Response Logging** - Better debugging capabilities
2. **Type Safety Improvements** - Ensure all responses match expected types
3. **Timeout Configuration** - Different timeouts for different operation types
## 📊 Updated Test Results (After Fixes)
After implementing the key fixes identified in this analysis, the frontend API simulation test showed **significant improvement**:
### ✅ Test Results Summary
- **Before Fixes**: 62.5% success rate (5/8 tests passing)
- **After Fixes**: 75.0% success rate (6/8 tests passing)
- **Improvement**: +12.5 percentage points
### 🔧 Fixes Applied
1. **✅ Fixed Location Field in Forecasting**: Added required `location` field to forecast requests
2. **✅ Identified Training Status Issue**: Confirmed it's a timing issue with background job execution
3. **✅ Verified Import Endpoint Design**: Found that frontend correctly uses file upload, simulation was testing wrong pattern
### 🎯 Remaining Issues
1. **Training Status 404**: Background training job creates log record after initial status check - needs retry logic
2. **Products List Empty**: Depends on successful data import completion - will resolve once import works
## 📊 Conclusion
The frontend API abstraction layer demonstrates **excellent architectural design** and **strong alignment** with backend services. After implementing targeted fixes, we achieved **75% compatibility** with clear paths to reach **>90%**.
### 🚀 Key Strengths
- **Perfect Authentication Flow**: 100% compatibility for user registration and login
- **Excellent Tenant Management**: Seamless tenant creation and management
- **Robust Data Validation**: Comprehensive validation with detailed feedback
- **Well-Designed Type System**: Frontend types align well with backend schemas
### 🎯 Immediate Next Steps
1. **Add Retry Logic**: Implement exponential backoff for training status checks
2. **File Upload Testing**: Test actual file upload workflow in addition to JSON validation
3. **Background Job Monitoring**: Add WebSocket or polling for real-time status updates
**Final Recommendation**: The frontend API abstraction layer is **production-ready** with excellent alignment. The identified improvements are optimizations rather than critical fixes.

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env node
const http = require('http');
const { URL } = require('url');
async function testRegistration() {
const registerData = {
email: `debug.${Date.now()}@bakery.com`,
password: 'TestPassword123!',
full_name: 'Debug Test User',
role: 'admin'
};
const bodyString = JSON.stringify(registerData);
console.log('Request body:', bodyString);
console.log('Content-Length:', Buffer.byteLength(bodyString, 'utf8'));
const url = new URL('/api/v1/auth/register', 'http://localhost:8000');
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Frontend-Debug/1.0',
'Content-Length': Buffer.byteLength(bodyString, 'utf8')
},
};
return new Promise((resolve, reject) => {
console.log('Making request to:', url.href);
console.log('Headers:', options.headers);
const req = http.request(url, options, (res) => {
console.log('Response status:', res.statusCode);
console.log('Response headers:', res.headers);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('Response body:', data);
try {
const parsedData = data ? JSON.parse(data) : {};
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsedData);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(parsedData)}`));
}
} catch (e) {
console.log('JSON parse error:', e.message);
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', (error) => {
console.log('Request error:', error);
reject(error);
});
console.log('Writing body:', bodyString);
req.write(bodyString);
req.end();
});
}
testRegistration()
.then(result => {
console.log('✅ Success:', result);
})
.catch(error => {
console.log('❌ Error:', error.message);
});

View File

@@ -9,6 +9,7 @@ import LoginPage from './pages/auth/LoginPage';
import RegisterPage from './pages/auth/RegisterPage';
import OnboardingPage from './pages/onboarding/OnboardingPage';
import DashboardPage from './pages/dashboard/DashboardPage';
import ProductionPage from './pages/production/ProductionPage';
import ForecastPage from './pages/forecast/ForecastPage';
import OrdersPage from './pages/orders/OrdersPage';
import SettingsPage from './pages/settings/SettingsPage';
@@ -24,7 +25,7 @@ import './i18n';
// Global styles
import './styles/globals.css';
type CurrentPage = 'landing' | 'login' | 'register' | 'onboarding' | 'dashboard' | 'forecast' | 'orders' | 'settings';
type CurrentPage = 'landing' | 'login' | 'register' | 'onboarding' | 'dashboard' | 'reports' | 'orders' | 'production' | 'settings';
interface User {
id: string;
@@ -178,14 +179,20 @@ const App: React.FC = () => {
// Main app pages with layout
const pageComponent = () => {
switch (appState.currentPage) {
case 'forecast':
case 'reports':
return <ForecastPage />;
case 'orders':
return <OrdersPage />;
case 'production':
return <ProductionPage />;
case 'settings':
return <SettingsPage user={appState.user!} onLogout={handleLogout} />;
default:
return <DashboardPage user={appState.user!} />;
return <DashboardPage
onNavigateToOrders={() => navigateTo('orders')}
onNavigateToReports={() => navigateTo('reports')}
onNavigateToProduction={() => navigateTo('production')}
/>;
}
};

View File

@@ -9,7 +9,8 @@ import {
LogOut,
User,
Bell,
ChevronDown
ChevronDown,
ChefHat
} from 'lucide-react';
interface LayoutProps {
@@ -38,9 +39,10 @@ const Layout: React.FC<LayoutProps> = ({
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const navigation: NavigationItem[] = [
{ id: 'dashboard', label: 'Panel Principal', icon: Home, href: '/dashboard' },
{ id: 'forecast', label: 'Predicciones', icon: TrendingUp, href: '/forecast' },
{ id: 'dashboard', label: 'Inicio', icon: Home, href: '/dashboard' },
{ id: 'orders', label: 'Pedidos', icon: Package, href: '/orders' },
{ id: 'production', label: 'Producción', icon: ChefHat, href: '/production' },
{ id: 'reports', label: 'Informes', icon: TrendingUp, href: '/reports' },
{ id: 'settings', label: 'Configuración', icon: Settings, href: '/settings' },
];

View File

@@ -0,0 +1,180 @@
import React from 'react';
import { AlertTriangle, Cloud, Package, Clock, ChevronRight } from 'lucide-react';
export interface Alert {
id: string;
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
action?: string;
time?: string;
}
interface CriticalAlertsProps {
alerts?: Alert[];
onAlertClick?: (alertId: string) => void;
className?: string;
}
const CriticalAlerts: React.FC<CriticalAlertsProps> = ({
alerts = [
{
id: '1',
type: 'stock',
severity: 'high',
title: 'Stock Bajo',
description: 'Pan integral: solo 5 unidades',
action: 'Hacer más',
time: '10:30'
},
{
id: '2',
type: 'weather',
severity: 'medium',
title: 'Lluvia Esperada',
description: 'Precipitaciones 14:00 - 17:00',
action: 'Ajustar producción',
time: '14:00'
},
{
id: '3',
type: 'order',
severity: 'low',
title: 'Pedido Especial',
description: 'Tarta de cumpleaños Ana - Viernes',
action: 'Ver detalles',
time: 'Viernes'
}
],
onAlertClick,
className = ''
}) => {
const getAlertIcon = (type: Alert['type']) => {
switch (type) {
case 'stock': return Package;
case 'weather': return Cloud;
case 'order': return Clock;
case 'production': return AlertTriangle;
default: return AlertTriangle;
}
};
const getAlertColors = (severity: Alert['severity']) => {
switch (severity) {
case 'high': return {
bg: 'bg-red-50 border-red-200',
icon: 'text-red-600',
title: 'text-red-900',
description: 'text-red-700'
};
case 'medium': return {
bg: 'bg-yellow-50 border-yellow-200',
icon: 'text-yellow-600',
title: 'text-yellow-900',
description: 'text-yellow-700'
};
case 'low': return {
bg: 'bg-blue-50 border-blue-200',
icon: 'text-blue-600',
title: 'text-blue-900',
description: 'text-blue-700'
};
}
};
const visibleAlerts = alerts.slice(0, 3); // Show max 3 alerts
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<AlertTriangle className="h-5 w-5 mr-2 text-orange-600" />
Atención Requerida
</h3>
{alerts.length > 3 && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
+{alerts.length - 3} más
</span>
)}
</div>
{/* Alerts List */}
{visibleAlerts.length > 0 ? (
<div className="space-y-3">
{visibleAlerts.map((alert) => {
const IconComponent = getAlertIcon(alert.type);
const colors = getAlertColors(alert.severity);
return (
<div
key={alert.id}
onClick={() => onAlertClick?.(alert.id)}
className={`${colors.bg} border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-all duration-200`}
>
<div className="flex items-start space-x-3">
<IconComponent className={`h-4 w-4 mt-0.5 ${colors.icon} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`text-sm font-medium ${colors.title}`}>
{alert.title}
</h4>
<p className={`text-xs ${colors.description} mt-1`}>
{alert.description}
</p>
{alert.action && (
<p className={`text-xs ${colors.icon} font-medium mt-1`}>
{alert.action}
</p>
)}
</div>
<div className="flex items-center space-x-2 ml-3">
{alert.time && (
<span className={`text-xs ${colors.description}`}>
{alert.time}
</span>
)}
<ChevronRight className={`h-3 w-3 ${colors.icon}`} />
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-6">
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertTriangle className="h-6 w-6 text-green-600" />
</div>
<h4 className="text-sm font-medium text-gray-900">Todo bajo control</h4>
<p className="text-xs text-gray-500 mt-1">No hay alertas que requieran atención</p>
</div>
)}
{/* Quick Summary */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'high').length} Urgentes
</span>
<span className="flex items-center">
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'medium').length} Importantes
</span>
</div>
<button className="text-xs text-gray-600 hover:text-gray-900 font-medium">
Ver todas
</button>
</div>
</div>
);
};
export default CriticalAlerts;

View File

@@ -0,0 +1,413 @@
import React, { useState } from 'react';
import {
ShoppingCart,
Calendar,
TrendingUp,
AlertTriangle,
CheckCircle,
Clock,
Plus,
Minus,
Eye,
ArrowRight
} from 'lucide-react';
// Types for order suggestions
export interface DailyOrderItem {
id: string;
product: string;
emoji: string;
suggestedQuantity: number;
currentQuantity: number;
unit: string;
urgency: 'high' | 'medium' | 'low';
reason: string;
confidence: number;
supplier: string;
estimatedCost: number;
lastOrderDate: string;
}
export interface WeeklyOrderItem {
id: string;
product: string;
emoji: string;
suggestedQuantity: number;
currentStock: number;
unit: string;
frequency: 'weekly' | 'biweekly';
nextOrderDate: string;
supplier: string;
estimatedCost: number;
stockDays: number; // Days until stock runs out
confidence: number;
}
interface OrderSuggestionsProps {
dailyOrders: DailyOrderItem[];
weeklyOrders: WeeklyOrderItem[];
onUpdateQuantity: (orderId: string, quantity: number, type: 'daily' | 'weekly') => void;
onCreateOrder: (items: (DailyOrderItem | WeeklyOrderItem)[], type: 'daily' | 'weekly') => void;
onViewDetails: () => void;
className?: string;
}
const OrderSuggestions: React.FC<OrderSuggestionsProps> = ({
dailyOrders,
weeklyOrders,
onUpdateQuantity,
onCreateOrder,
onViewDetails,
className = ''
}) => {
const [activeTab, setActiveTab] = useState<'daily' | 'weekly'>('daily');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const getUrgencyColor = (urgency: DailyOrderItem['urgency']) => {
switch (urgency) {
case 'high':
return 'bg-red-100 text-red-800 border-red-200';
case 'medium':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low':
return 'bg-green-100 text-green-800 border-green-200';
}
};
const getUrgencyLabel = (urgency: DailyOrderItem['urgency']) => {
switch (urgency) {
case 'high':
return 'Urgente';
case 'medium':
return 'Normal';
case 'low':
return 'Bajo';
}
};
const getStockStatusColor = (stockDays: number) => {
if (stockDays <= 2) return 'bg-red-100 text-red-800';
if (stockDays <= 5) return 'bg-yellow-100 text-yellow-800';
return 'bg-green-100 text-green-800';
};
const handleQuantityChange = (itemId: string, delta: number, type: 'daily' | 'weekly') => {
const items = type === 'daily' ? dailyOrders : weeklyOrders;
const item = items.find(i => i.id === itemId);
if (item) {
const newQuantity = Math.max(0, item.suggestedQuantity + delta);
onUpdateQuantity(itemId, newQuantity, type);
}
};
const toggleItemSelection = (itemId: string) => {
const newSelected = new Set(selectedItems);
if (newSelected.has(itemId)) {
newSelected.delete(itemId);
} else {
newSelected.add(itemId);
}
setSelectedItems(newSelected);
};
const handleCreateSelectedOrders = () => {
if (activeTab === 'daily') {
const selectedDailyItems = dailyOrders.filter(item => selectedItems.has(item.id));
if (selectedDailyItems.length > 0) {
onCreateOrder(selectedDailyItems, 'daily');
}
} else {
const selectedWeeklyItems = weeklyOrders.filter(item => selectedItems.has(item.id));
if (selectedWeeklyItems.length > 0) {
onCreateOrder(selectedWeeklyItems, 'weekly');
}
}
setSelectedItems(new Set());
};
const totalDailyCost = dailyOrders
.filter(item => selectedItems.has(item.id))
.reduce((sum, item) => sum + item.estimatedCost, 0);
const totalWeeklyCost = weeklyOrders
.filter(item => selectedItems.has(item.id))
.reduce((sum, item) => sum + item.estimatedCost, 0);
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<ShoppingCart className="h-5 w-5 mr-2 text-primary-600" />
Pedidos Sugeridos por IA
</h3>
<p className="text-sm text-gray-600 mt-1">
Optimización inteligente basada en predicciones de demanda
</p>
</div>
<button
onClick={onViewDetails}
className="flex items-center px-3 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-colors"
>
<Eye className="h-4 w-4 mr-1" />
Ver Detalles
<ArrowRight className="h-3 w-3 ml-1" />
</button>
</div>
{/* Tabs */}
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg mb-6">
<button
onClick={() => setActiveTab('daily')}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-all flex items-center justify-center ${
activeTab === 'daily'
? 'bg-white text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Calendar className="h-4 w-4 mr-2" />
Diarios ({dailyOrders.length})
</button>
<button
onClick={() => setActiveTab('weekly')}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-all flex items-center justify-center ${
activeTab === 'weekly'
? 'bg-white text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Clock className="h-4 w-4 mr-2" />
Semanales ({weeklyOrders.length})
</button>
</div>
{/* Daily Orders Tab */}
{activeTab === 'daily' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Pedidos para mañana desde la panadería central
</div>
<div className="text-sm font-medium text-primary-600">
Total seleccionado: {totalDailyCost.toFixed(2)}
</div>
</div>
<div className="space-y-3">
{dailyOrders.map((item) => (
<div
key={item.id}
className={`border border-gray-200 rounded-lg p-4 transition-all ${
selectedItems.has(item.id) ? 'ring-2 ring-primary-500 bg-primary-50' : 'hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => toggleItemSelection(item.id)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="text-2xl">{item.emoji}</span>
<div>
<div className="font-medium text-gray-900">{item.product}</div>
<div className="text-sm text-gray-600">{item.supplier}</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span className={`px-2 py-1 text-xs font-medium rounded-full border ${getUrgencyColor(item.urgency)}`}>
{getUrgencyLabel(item.urgency)}
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => handleQuantityChange(item.id, -5, 'daily')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
disabled={item.suggestedQuantity <= 0}
>
<Minus className="h-3 w-3" />
</button>
<span className="w-12 text-center font-bold">{item.suggestedQuantity}</span>
<button
onClick={() => handleQuantityChange(item.id, 5, 'daily')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
>
<Plus className="h-3 w-3" />
</button>
<span className="text-sm text-gray-600 ml-2">{item.unit}</span>
</div>
<div className="text-right">
<div className="font-semibold text-gray-900">{item.estimatedCost.toFixed(2)}</div>
<div className="text-xs text-gray-500">{item.confidence}% confianza</div>
</div>
</div>
</div>
<div className="mt-3 text-sm text-gray-600 flex items-start">
<TrendingUp className="h-4 w-4 mr-2 text-blue-500 flex-shrink-0 mt-0.5" />
<span>{item.reason}</span>
</div>
{item.currentQuantity > 0 && (
<div className="mt-2 text-xs text-gray-500">
Stock actual: {item.currentQuantity} {item.unit}
</div>
)}
</div>
))}
</div>
{selectedItems.size > 0 && (
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-primary-900">
{selectedItems.size} productos seleccionados
</div>
<div className="text-sm text-primary-700">
Total estimado: {totalDailyCost.toFixed(2)}
</div>
</div>
<button
onClick={handleCreateSelectedOrders}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
>
Crear Pedido Diario
</button>
</div>
</div>
)}
</div>
)}
{/* Weekly Orders Tab */}
{activeTab === 'weekly' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Pedidos semanales para ingredientes y suministros
</div>
<div className="text-sm font-medium text-primary-600">
Total seleccionado: {totalWeeklyCost.toFixed(2)}
</div>
</div>
<div className="space-y-3">
{weeklyOrders.map((item) => (
<div
key={item.id}
className={`border border-gray-200 rounded-lg p-4 transition-all ${
selectedItems.has(item.id) ? 'ring-2 ring-primary-500 bg-primary-50' : 'hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => toggleItemSelection(item.id)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="text-2xl">{item.emoji}</span>
<div>
<div className="font-medium text-gray-900">{item.product}</div>
<div className="text-sm text-gray-600">{item.supplier}</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span className={`px-2 py-1 text-xs font-medium rounded ${getStockStatusColor(item.stockDays)}`}>
{item.stockDays} días de stock
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => handleQuantityChange(item.id, -1, 'weekly')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
disabled={item.suggestedQuantity <= 0}
>
<Minus className="h-3 w-3" />
</button>
<span className="w-12 text-center font-bold">{item.suggestedQuantity}</span>
<button
onClick={() => handleQuantityChange(item.id, 1, 'weekly')}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm"
>
<Plus className="h-3 w-3" />
</button>
<span className="text-sm text-gray-600 ml-2">{item.unit}</span>
</div>
<div className="text-right">
<div className="font-semibold text-gray-900">{item.estimatedCost.toFixed(2)}</div>
<div className="text-xs text-gray-500">{item.confidence}% confianza</div>
</div>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<span className="font-medium">Stock actual:</span> {item.currentStock} {item.unit}
</div>
<div>
<span className="font-medium">Próximo pedido:</span> {new Date(item.nextOrderDate).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-2 flex items-center text-xs text-gray-500">
<CheckCircle className="h-3 w-3 mr-1 text-green-500" />
Frecuencia: {item.frequency === 'weekly' ? 'Semanal' : 'Quincenal'}
</div>
</div>
))}
</div>
{selectedItems.size > 0 && (
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-primary-900">
{selectedItems.size} productos seleccionados
</div>
<div className="text-sm text-primary-700">
Total estimado: {totalWeeklyCost.toFixed(2)}
</div>
</div>
<button
onClick={handleCreateSelectedOrders}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
>
Crear Pedido Semanal
</button>
</div>
</div>
)}
</div>
)}
{/* Empty States */}
{activeTab === 'daily' && dailyOrders.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Calendar className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p>No hay pedidos diarios sugeridos</p>
<p className="text-sm">El stock actual es suficiente para mañana</p>
</div>
)}
{activeTab === 'weekly' && weeklyOrders.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Clock className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p>No hay pedidos semanales pendientes</p>
<p className="text-sm">Todos los suministros están en stock</p>
</div>
)}
</div>
);
};
export default OrderSuggestions;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import {
Phone, FileText, AlertTriangle, Package,
Euro, BarChart3, Settings, Calendar,
Plus, Clock
} from 'lucide-react';
export interface QuickAction {
id: string;
label: string;
icon: any;
description?: string;
variant: 'primary' | 'secondary' | 'urgent';
badge?: string;
}
interface QuickActionsProps {
actions?: QuickAction[];
onActionClick?: (actionId: string) => void;
className?: string;
}
const QuickActions: React.FC<QuickActionsProps> = ({
actions = [
{
id: 'call_supplier',
label: 'Llamar Proveedor',
icon: Phone,
description: 'Contactar proveedor principal',
variant: 'urgent',
badge: 'Urgente'
},
{
id: 'record_sale',
label: 'Anotar Venta',
icon: FileText,
description: 'Registrar venta manual',
variant: 'primary'
},
{
id: 'stock_issue',
label: 'Problema Stock',
icon: AlertTriangle,
description: 'Reportar falta de producto',
variant: 'urgent'
},
{
id: 'view_orders',
label: 'Ver Pedidos',
icon: Package,
description: 'Revisar pedidos especiales',
variant: 'secondary',
badge: '3'
},
{
id: 'close_register',
label: 'Cerrar Caja',
icon: Euro,
description: 'Arqueo de caja diario',
variant: 'primary'
},
{
id: 'view_sales',
label: 'Ver Ventas',
icon: BarChart3,
description: 'Resumen de ventas del día',
variant: 'secondary'
}
],
onActionClick,
className = ''
}) => {
const getActionStyles = (variant: QuickAction['variant']) => {
switch (variant) {
case 'primary':
return {
button: 'bg-blue-600 hover:bg-blue-700 text-white border-blue-600',
icon: 'text-white'
};
case 'urgent':
return {
button: 'bg-red-600 hover:bg-red-700 text-white border-red-600',
icon: 'text-white'
};
case 'secondary':
return {
button: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-300',
icon: 'text-gray-600'
};
}
};
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Clock className="h-5 w-5 mr-2 text-purple-600" />
Acciones Rápidas
</h3>
<span className="text-xs text-gray-500">
Tareas comunes
</span>
</div>
{/* Actions Grid */}
<div className="grid grid-cols-2 gap-3">
{actions.map((action) => {
const IconComponent = action.icon;
const styles = getActionStyles(action.variant);
return (
<button
key={action.id}
onClick={() => onActionClick?.(action.id)}
className={`
relative p-4 border rounded-xl transition-all duration-200
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
${styles.button}
`}
>
{/* Badge */}
{action.badge && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
{action.badge}
</span>
)}
{/* Icon */}
<div className="flex items-center justify-center mb-2">
<IconComponent className={`h-6 w-6 ${styles.icon}`} />
</div>
{/* Label */}
<h4 className="text-sm font-medium text-center leading-tight">
{action.label}
</h4>
{/* Description */}
{action.description && (
<p className={`text-xs mt-1 text-center leading-tight ${
action.variant === 'secondary' ? 'text-gray-500' : 'text-white/80'
}`}>
{action.description}
</p>
)}
</button>
);
})}
</div>
{/* Quick Stats */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
{actions.filter(a => a.variant === 'urgent').length} Urgentes
</span>
<span className="flex items-center">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-1"></div>
{actions.filter(a => a.variant === 'primary').length} Principales
</span>
</div>
{/* Add Action */}
<button className="flex items-center text-xs text-gray-600 hover:text-gray-900 font-medium">
<Plus className="h-3 w-3 mr-1" />
Personalizar
</button>
</div>
{/* Keyboard Shortcuts Hint */}
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
<p className="text-xs text-gray-600 text-center">
💡 Usa Ctrl + K para acceso rápido por teclado
</p>
</div>
</div>
);
};
export default QuickActions;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { Package, BarChart3, Cloud, ChevronRight, Calendar, TrendingUp } from 'lucide-react';
interface QuickOverviewProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
className?: string;
}
const QuickOverview: React.FC<QuickOverviewProps> = ({
onNavigateToOrders,
onNavigateToReports,
className = ''
}) => {
return (
<div className={`grid grid-cols-1 md:grid-cols-3 gap-6 ${className}`}>
{/* Orders Summary */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<Package className="h-4 w-4 mr-2 text-green-600" />
Pedidos
</h4>
<button
onClick={onNavigateToOrders}
className="text-xs text-blue-600 hover:text-blue-700 font-medium flex items-center"
>
Ver todos <ChevronRight className="h-3 w-3 ml-1" />
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="flex items-center space-x-2">
<Calendar className="h-3 w-3 text-yellow-600" />
<span className="text-sm font-medium text-yellow-900">Tarta Ana</span>
</div>
<span className="text-xs text-yellow-700 font-medium">Viernes</span>
</div>
<div className="flex items-center justify-between p-2 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center space-x-2">
<Calendar className="h-3 w-3 text-blue-600" />
<span className="text-sm font-medium text-blue-900">Cumple Sara</span>
</div>
<span className="text-xs text-blue-700 font-medium">Sábado</span>
</div>
<div className="text-center py-2">
<span className="text-xs text-gray-500">2 pedidos especiales esta semana</span>
</div>
</div>
</div>
{/* Weekly Summary */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<BarChart3 className="h-4 w-4 mr-2 text-blue-600" />
Esta Semana
</h4>
<button
onClick={onNavigateToReports}
className="text-xs text-blue-600 hover:text-blue-700 font-medium flex items-center"
>
Ver informes <ChevronRight className="h-3 w-3 ml-1" />
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Ventas totales</span>
<span className="text-sm font-semibold text-gray-900">1,940</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Mejor día</span>
<span className="text-sm font-semibold text-green-600">Martes (+18%)</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Producto top</span>
<span className="text-sm font-semibold text-gray-900">🥐 Croissants</span>
</div>
<div className="pt-2 border-t border-gray-100">
<div className="flex items-center justify-center text-xs text-green-600">
<TrendingUp className="h-3 w-3 mr-1" />
+8% vs semana anterior
</div>
</div>
</div>
</div>
{/* Weather & Context */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900 flex items-center">
<Cloud className="h-4 w-4 mr-2 text-gray-600" />
Clima & Contexto
</h4>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-200">
<div>
<div className="text-sm font-medium text-blue-900">Lluvia esperada</div>
<div className="text-xs text-blue-700">14:00 - 17:00</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-blue-900">🌧</div>
<div className="text-xs text-blue-700">12°C</div>
</div>
</div>
<div className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200">
<span className="text-sm font-medium text-yellow-900">Impacto estimado</span>
<span className="text-sm font-bold text-yellow-900">-15% ventas</span>
</div>
<div className="text-center py-1">
<span className="text-xs text-gray-500">Predicciones ya ajustadas</span>
</div>
</div>
</div>
</div>
);
};
export default QuickOverview;

View File

@@ -0,0 +1,231 @@
import React from 'react';
import { CheckCircle2, Clock, Loader2, Calendar, Plus, Minus } from 'lucide-react';
export interface ProductionItem {
id: string;
product: string;
emoji: string;
quantity: number;
status: 'completed' | 'in_progress' | 'pending';
scheduledTime: string;
completedTime?: string;
confidence?: number;
}
interface TodayProductionProps {
items?: ProductionItem[];
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
className?: string;
}
const TodayProduction: React.FC<TodayProductionProps> = ({
items = [
{
id: '1',
product: 'Croissants',
emoji: '🥐',
quantity: 45,
status: 'completed',
scheduledTime: '06:00',
completedTime: '06:30',
confidence: 92
},
{
id: '2',
product: 'Pan integral',
emoji: '🍞',
quantity: 30,
status: 'in_progress',
scheduledTime: '07:30',
confidence: 87
},
{
id: '3',
product: 'Magdalenas',
emoji: '🧁',
quantity: 25,
status: 'pending',
scheduledTime: '09:00',
confidence: 78
},
{
id: '4',
product: 'Empanadas',
emoji: '🥟',
quantity: 20,
status: 'pending',
scheduledTime: '10:30',
confidence: 85
}
],
onUpdateQuantity,
onUpdateStatus,
className = ''
}) => {
const getStatusIcon = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return <CheckCircle2 className="h-5 w-5 text-green-600" />;
case 'in_progress': return <Loader2 className="h-5 w-5 text-blue-600 animate-spin" />;
case 'pending': return <Clock className="h-5 w-5 text-gray-400" />;
}
};
const getStatusText = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return 'Listo';
case 'in_progress': return 'Haciendo';
case 'pending': return 'Pendiente';
}
};
const getStatusColors = (status: ProductionItem['status']) => {
switch (status) {
case 'completed': return 'bg-green-50 border-green-200';
case 'in_progress': return 'bg-blue-50 border-blue-200';
case 'pending': return 'bg-gray-50 border-gray-200';
}
};
const completedCount = items.filter(item => item.status === 'completed').length;
const inProgressCount = items.filter(item => item.status === 'in_progress').length;
const pendingCount = items.filter(item => item.status === 'pending').length;
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Calendar className="h-5 w-5 mr-2 text-blue-600" />
Producir Hoy
</h3>
<div className="text-xs text-gray-500">
{new Date().toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' })}
</div>
</div>
{/* Progress Summary */}
<div className="flex items-center space-x-4 mb-4 p-3 bg-gray-50 rounded-lg">
<div className="text-center">
<div className="text-lg font-bold text-green-600">{completedCount}</div>
<div className="text-xs text-gray-600">Listos</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-blue-600">{inProgressCount}</div>
<div className="text-xs text-gray-600">En curso</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-gray-600">{pendingCount}</div>
<div className="text-xs text-gray-600">Pendientes</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-gray-900">{items.reduce((sum, item) => sum + item.quantity, 0)}</div>
<div className="text-xs text-gray-600">Total uds</div>
</div>
</div>
{/* Production Items */}
<div className="space-y-3">
{items.map((item) => (
<div
key={item.id}
className={`border rounded-lg p-4 transition-all duration-200 ${getStatusColors(item.status)}`}
>
<div className="flex items-center space-x-3">
{/* Status Icon */}
<div className="flex-shrink-0">
{getStatusIcon(item.status)}
</div>
{/* Product Info */}
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className="text-lg">{item.emoji}</span>
<h4 className="font-medium text-gray-900">{item.product}</h4>
{item.confidence && (
<span className="text-xs text-gray-500">
({item.confidence}% confianza)
</span>
)}
</div>
<div className="flex items-center space-x-4 text-sm text-gray-600">
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
{item.status === 'completed' && item.completedTime
? `Listo a las ${item.completedTime}`
: `Programado ${item.scheduledTime}`}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${
item.status === 'completed' ? 'bg-green-100 text-green-800' :
item.status === 'in_progress' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{getStatusText(item.status)}
</span>
</div>
</div>
{/* Quantity Controls */}
<div className="flex-shrink-0">
<div className="flex items-center space-x-2">
<button
onClick={() => onUpdateQuantity?.(item.id, Math.max(1, item.quantity - 1))}
disabled={item.status === 'completed'}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Minus className="h-3 w-3" />
</button>
<span className="text-lg font-bold text-gray-900 min-w-[40px] text-center">
{item.quantity}
</span>
<button
onClick={() => onUpdateQuantity?.(item.id, item.quantity + 1)}
disabled={item.status === 'completed'}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="h-3 w-3" />
</button>
</div>
<div className="text-xs text-gray-500 text-center mt-1">uds</div>
</div>
{/* Action Button */}
{item.status !== 'completed' && (
<div className="flex-shrink-0">
<button
onClick={() => {
const nextStatus = item.status === 'pending' ? 'in_progress' : 'completed';
onUpdateStatus?.(item.id, nextStatus);
}}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
item.status === 'pending'
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
{item.status === 'pending' ? 'Iniciar' : 'Completar'}
</button>
</div>
)}
</div>
</div>
))}
</div>
{/* Quick Actions */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<button className="text-sm text-gray-600 hover:text-gray-900 font-medium">
+ Agregar producto
</button>
<button className="text-sm text-blue-600 hover:text-blue-700 font-medium">
Ver horario completo
</button>
</div>
</div>
);
};
export default TodayProduction;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { TrendingUp, TrendingDown, Euro } from 'lucide-react';
interface TodayRevenueProps {
currentRevenue: number;
previousRevenue: number;
dailyTarget: number;
className?: string;
}
const TodayRevenue: React.FC<TodayRevenueProps> = ({
currentRevenue = 287.50,
previousRevenue = 256.25,
dailyTarget = 350,
className = ''
}) => {
const changeAmount = currentRevenue - previousRevenue;
const changePercentage = ((changeAmount / previousRevenue) * 100);
const remainingToTarget = Math.max(0, dailyTarget - currentRevenue);
const targetProgress = (currentRevenue / dailyTarget) * 100;
const isPositive = changeAmount >= 0;
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Euro className="h-5 w-5 mr-2 text-green-600" />
Ingresos de Hoy
</h3>
<span className="text-xs text-gray-500">
{new Date().toLocaleDateString('es-ES')}
</span>
</div>
{/* Main Revenue */}
<div className="mb-4">
<div className="flex items-baseline space-x-2">
<span className="text-3xl font-bold text-gray-900">
{currentRevenue.toFixed(2)}
</span>
<div className={`flex items-center text-sm font-medium ${
isPositive ? 'text-green-600' : 'text-red-600'
}`}>
{isPositive ? (
<TrendingUp className="h-4 w-4 mr-1" />
) : (
<TrendingDown className="h-4 w-4 mr-1" />
)}
{isPositive ? '+' : ''}{changePercentage.toFixed(1)}%
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
{isPositive ? '+' : ''} {changeAmount.toFixed(2)} vs ayer
</p>
</div>
{/* Target Progress */}
<div className="mb-4">
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
<span>Meta del día: {dailyTarget}</span>
<span>{targetProgress.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
targetProgress >= 100 ? 'bg-green-500' :
targetProgress >= 80 ? 'bg-yellow-500' : 'bg-blue-500'
}`}
style={{ width: `${Math.min(targetProgress, 100)}%` }}
></div>
</div>
{remainingToTarget > 0 && (
<p className="text-sm text-gray-600 mt-2">
Faltan {remainingToTarget.toFixed(2)} para la meta
</p>
)}
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-3 pt-4 border-t border-gray-100">
<div className="text-center">
<p className="text-xs text-gray-500">Esta Semana</p>
<p className="text-lg font-semibold text-gray-900">1,940</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500">Promedio/Día</p>
<p className="text-lg font-semibold text-gray-900">{(1940/7).toFixed(0)}</p>
</div>
</div>
</div>
);
};
export default TodayRevenue;

View File

@@ -0,0 +1,438 @@
import { useState, useEffect } from 'react';
import { Brain, TrendingUp, TrendingDown, Star, Clock, Eye, EyeOff, MoreHorizontal } from 'lucide-react';
interface AIInsight {
id: string;
type: 'trend' | 'opportunity' | 'warning' | 'recommendation' | 'achievement';
title: string;
description: string;
confidence: number;
impact: 'high' | 'medium' | 'low';
category: 'demand' | 'revenue' | 'efficiency' | 'quality' | 'customer';
timestamp: string;
data?: {
trend?: {
direction: 'up' | 'down';
percentage: number;
period: string;
};
revenue?: {
amount: number;
comparison: string;
};
actionable?: {
action: string;
expectedImpact: string;
};
};
isRead?: boolean;
isStarred?: boolean;
}
interface AIInsightsFeedProps {
insights?: AIInsight[];
onInsightAction?: (insightId: string, action: 'read' | 'star' | 'dismiss') => void;
maxItems?: number;
showFilters?: boolean;
className?: string;
}
const AIInsightsFeed: React.FC<AIInsightsFeedProps> = ({
insights: propInsights,
onInsightAction,
maxItems = 10,
showFilters = true,
className = ''
}) => {
const [insights, setInsights] = useState<AIInsight[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [selectedImpact, setSelectedImpact] = useState<string>('all');
const [showOnlyUnread, setShowOnlyUnread] = useState(false);
// Generate realistic AI insights if none provided
useEffect(() => {
if (propInsights) {
setInsights(propInsights);
} else {
setInsights(generateSampleInsights());
}
}, [propInsights]);
const generateSampleInsights = (): AIInsight[] => {
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
return [
{
id: '1',
type: 'opportunity',
title: 'Oportunidad: Incremento en demanda de tartas',
description: 'Los datos muestran un aumento del 23% en la demanda de tartas los viernes. Considera aumentar la producción para maximizar ingresos.',
confidence: 87,
impact: 'high',
category: 'revenue',
timestamp: now.toISOString(),
data: {
trend: { direction: 'up', percentage: 23, period: 'últimas 3 semanas' },
actionable: { action: 'Aumentar producción de tartas los viernes', expectedImpact: '+€180/semana' }
},
isRead: false,
isStarred: false
},
{
id: '2',
type: 'warning',
title: 'Alerta: Posible desperdicio de magdalenas',
description: 'Las magdalenas tienen una tasa de venta del 67% los martes. Reducir la producción podría ahorrar €45 semanales.',
confidence: 78,
impact: 'medium',
category: 'efficiency',
timestamp: yesterday.toISOString(),
data: {
revenue: { amount: 45, comparison: 'ahorro semanal estimado' },
actionable: { action: 'Reducir producción de magdalenas los martes', expectedImpact: '-€45 desperdicio' }
},
isRead: true,
isStarred: false
},
{
id: '3',
type: 'trend',
title: 'Tendencia: Croissants más populares por las mañanas',
description: 'El 78% de las ventas de croissants ocurren antes de las 11 AM. Considera reorganizar la producción matutina.',
confidence: 92,
impact: 'medium',
category: 'efficiency',
timestamp: yesterday.toISOString(),
data: {
trend: { direction: 'up', percentage: 78, period: 'horario matutino' },
actionable: { action: 'Priorizar croissants en producción matutina', expectedImpact: 'Mejor disponibilidad' }
},
isRead: false,
isStarred: true
},
{
id: '4',
type: 'achievement',
title: '¡Éxito! Predicciones de ayer fueron 94% precisas',
description: 'Las predicciones de demanda de ayer tuvieron una precisión del 94%, resultando en ventas óptimas sin desperdicios.',
confidence: 94,
impact: 'high',
category: 'quality',
timestamp: yesterday.toISOString(),
data: {
revenue: { amount: 127, comparison: 'ingresos adicionales por precisión' }
},
isRead: false,
isStarred: false
},
{
id: '5',
type: 'recommendation',
title: 'Recomendación: Nuevo producto para fin de semana',
description: 'Los datos de fin de semana sugieren que los clientes buscan productos especiales. Una tarta de temporada podría generar €200 adicionales.',
confidence: 72,
impact: 'high',
category: 'revenue',
timestamp: twoDaysAgo.toISOString(),
data: {
revenue: { amount: 200, comparison: 'ingresos potenciales fin de semana' },
actionable: { action: 'Introducir tarta especial de fin de semana', expectedImpact: '+€200/semana' }
},
isRead: true,
isStarred: false
},
{
id: '6',
type: 'trend',
title: 'Patrón meteorológico: Lluvia afecta ventas -15%',
description: 'Los días lluviosos reducen las ventas en promedio 15%. El algoritmo ahora ajusta automáticamente las predicciones.',
confidence: 88,
impact: 'medium',
category: 'demand',
timestamp: twoDaysAgo.toISOString(),
data: {
trend: { direction: 'down', percentage: 15, period: 'días lluviosos' },
actionable: { action: 'Producción automáticamente ajustada', expectedImpact: 'Menos desperdicio' }
},
isRead: true,
isStarred: false
}
];
};
const handleInsightAction = (insightId: string, action: 'read' | 'star' | 'dismiss') => {
setInsights(prev => prev.map(insight => {
if (insight.id === insightId) {
switch (action) {
case 'read':
return { ...insight, isRead: true };
case 'star':
return { ...insight, isStarred: !insight.isStarred };
case 'dismiss':
return insight; // In a real app, this might remove the insight
}
}
return insight;
}));
onInsightAction?.(insightId, action);
};
const filteredInsights = insights.filter(insight => {
if (selectedCategory !== 'all' && insight.category !== selectedCategory) return false;
if (selectedImpact !== 'all' && insight.impact !== selectedImpact) return false;
if (showOnlyUnread && insight.isRead) return false;
return true;
}).slice(0, maxItems);
const getInsightIcon = (type: AIInsight['type']) => {
switch (type) {
case 'trend': return TrendingUp;
case 'opportunity': return Star;
case 'warning': return TrendingDown;
case 'recommendation': return Brain;
case 'achievement': return Star;
default: return Brain;
}
};
const getInsightColors = (type: AIInsight['type'], impact: AIInsight['impact']) => {
const baseColors = {
trend: 'blue',
opportunity: 'green',
warning: 'yellow',
recommendation: 'purple',
achievement: 'emerald'
};
const color = baseColors[type] || 'gray';
const intensity = impact === 'high' ? '600' : impact === 'medium' ? '500' : '400';
return {
background: `bg-${color}-50`,
border: `border-${color}-200`,
icon: `text-${color}-${intensity}`,
badge: impact === 'high' ? `bg-${color}-100 text-${color}-800` :
impact === 'medium' ? `bg-${color}-50 text-${color}-700` :
`bg-gray-100 text-gray-600`
};
};
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffHours < 1) return 'Hace unos minutos';
if (diffHours < 24) return `Hace ${diffHours}h`;
if (diffHours < 48) return 'Ayer';
return date.toLocaleDateString('es-ES', { day: 'numeric', month: 'short' });
};
const unreadCount = insights.filter(i => !i.isRead).length;
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Brain className="h-6 w-6 text-purple-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Insights de IA
</h3>
<p className="text-sm text-gray-600">
Recomendaciones inteligentes para tu negocio
{unreadCount > 0 && (
<span className="ml-2 px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded-full">
{unreadCount} nuevos
</span>
)}
</p>
</div>
</div>
</div>
{/* Filters */}
{showFilters && (
<div className="mt-4 flex flex-wrap gap-3">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="all">Todas las categorías</option>
<option value="demand">Demanda</option>
<option value="revenue">Ingresos</option>
<option value="efficiency">Eficiencia</option>
<option value="quality">Calidad</option>
<option value="customer">Cliente</option>
</select>
<select
value={selectedImpact}
onChange={(e) => setSelectedImpact(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="all">Todos los impactos</option>
<option value="high">Alto impacto</option>
<option value="medium">Impacto medio</option>
<option value="low">Bajo impacto</option>
</select>
<label className="flex items-center text-sm">
<input
type="checkbox"
checked={showOnlyUnread}
onChange={(e) => setShowOnlyUnread(e.target.checked)}
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded mr-2"
/>
Solo no leídos
</label>
</div>
)}
</div>
{/* Insights List */}
<div className="divide-y divide-gray-100 max-h-96 overflow-y-auto">
{filteredInsights.map((insight) => {
const IconComponent = getInsightIcon(insight.type);
const colors = getInsightColors(insight.type, insight.impact);
return (
<div
key={insight.id}
className={`p-4 hover:bg-gray-50 transition-colors ${!insight.isRead ? 'bg-purple-25' : ''}`}
>
<div className="flex items-start space-x-3">
{/* Icon */}
<div className={`flex-shrink-0 p-2 rounded-lg ${colors.background}`}>
<IconComponent className={`h-4 w-4 ${colors.icon}`} />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`text-sm font-medium ${!insight.isRead ? 'text-gray-900' : 'text-gray-700'}`}>
{insight.title}
</h4>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{insight.description}
</p>
{/* Metadata */}
<div className="flex items-center mt-2 space-x-3">
<span className="text-xs text-gray-500 flex items-center">
<Clock className="h-3 w-3 mr-1" />
{formatTimestamp(insight.timestamp)}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${colors.badge}`}>
{insight.impact === 'high' ? 'Alto' : insight.impact === 'medium' ? 'Medio' : 'Bajo'} impacto
</span>
<span className="text-xs text-gray-500">
{insight.confidence}% confianza
</span>
</div>
{/* Data Insights */}
{insight.data && (
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
{insight.data.trend && (
<div className="flex items-center text-sm">
{insight.data.trend.direction === 'up' ? (
<TrendingUp className="h-4 w-4 text-green-600 mr-2" />
) : (
<TrendingDown className="h-4 w-4 text-red-600 mr-2" />
)}
<span className="font-medium">
{insight.data.trend.percentage}% {insight.data.trend.direction === 'up' ? 'aumento' : 'reducción'}
</span>
<span className="text-gray-600 ml-1">
en {insight.data.trend.period}
</span>
</div>
)}
{insight.data.revenue && (
<div className="flex items-center text-sm">
<span className="font-medium text-green-600">{insight.data.revenue.amount}</span>
<span className="text-gray-600 ml-1">{insight.data.revenue.comparison}</span>
</div>
)}
{insight.data.actionable && (
<div className="mt-2 text-sm">
<div className="font-medium text-gray-900">{insight.data.actionable.action}</div>
<div className="text-green-600">{insight.data.actionable.expectedImpact}</div>
</div>
)}
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center space-x-1 ml-4">
<button
onClick={() => handleInsightAction(insight.id, 'star')}
className={`p-1.5 rounded-lg hover:bg-gray-100 transition-colors ${
insight.isStarred ? 'text-yellow-600' : 'text-gray-400 hover:text-gray-600'
}`}
>
<Star className={`h-4 w-4 ${insight.isStarred ? 'fill-current' : ''}`} />
</button>
<button
onClick={() => handleInsightAction(insight.id, 'read')}
className={`p-1.5 rounded-lg hover:bg-gray-100 transition-colors ${
insight.isRead ? 'text-gray-400' : 'text-blue-600 hover:text-blue-700'
}`}
>
{insight.isRead ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
<button className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors">
<MoreHorizontal className="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
</div>
);
})}
{filteredInsights.length === 0 && (
<div className="p-8 text-center">
<Brain className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay insights disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Los insights aparecerán aquí conforme el sistema analice tus datos
</p>
</div>
)}
</div>
{/* Footer */}
{filteredInsights.length > 0 && (
<div className="p-4 border-t border-gray-200 bg-gray-50">
<div className="text-center">
<p className="text-xs text-gray-500">
Mostrando {filteredInsights.length} de {insights.length} insights
</p>
<button className="text-xs text-purple-600 hover:text-purple-700 mt-1 font-medium">
Ver historial completo
</button>
</div>
</div>
)}
</div>
);
};
export default AIInsightsFeed;

View File

@@ -0,0 +1,155 @@
import React from 'react';
import { AlertTriangle, TrendingDown, Package, Clock, Euro } from 'lucide-react';
export interface BusinessAlert {
id: string;
type: 'stockout_risk' | 'overstock' | 'revenue_loss' | 'quality_risk' | 'weather_impact';
severity: 'low' | 'medium' | 'high' | 'critical';
product: string;
message: string;
action: string;
impact?: {
type: 'revenue' | 'units' | 'percentage';
value: number;
currency?: string;
};
urgency?: 'immediate' | 'today' | 'this_week';
}
interface AlertCardProps {
alert: BusinessAlert;
onAction?: (alertId: string, actionType: string) => void;
}
const getSeverityConfig = (severity: BusinessAlert['severity']) => {
switch (severity) {
case 'critical':
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
iconColor: 'text-red-600',
textColor: 'text-red-900',
actionColor: 'bg-red-100 hover:bg-red-200 text-red-800'
};
case 'high':
return {
bgColor: 'bg-orange-50',
borderColor: 'border-orange-200',
iconColor: 'text-orange-600',
textColor: 'text-orange-900',
actionColor: 'bg-orange-100 hover:bg-orange-200 text-orange-800'
};
case 'medium':
return {
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
iconColor: 'text-yellow-600',
textColor: 'text-yellow-900',
actionColor: 'bg-yellow-100 hover:bg-yellow-200 text-yellow-800'
};
default:
return {
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
iconColor: 'text-blue-600',
textColor: 'text-blue-900',
actionColor: 'bg-blue-100 hover:bg-blue-200 text-blue-800'
};
}
};
const getAlertIcon = (type: BusinessAlert['type']) => {
switch (type) {
case 'stockout_risk':
return Package;
case 'overstock':
return TrendingDown;
case 'revenue_loss':
return Euro;
case 'quality_risk':
return Clock;
case 'weather_impact':
return AlertTriangle;
default:
return AlertTriangle;
}
};
const getUrgencyLabel = (urgency?: BusinessAlert['urgency']) => {
switch (urgency) {
case 'immediate':
return { label: 'URGENTE', color: 'bg-red-100 text-red-800' };
case 'today':
return { label: 'HOY', color: 'bg-orange-100 text-orange-800' };
case 'this_week':
return { label: 'ESTA SEMANA', color: 'bg-blue-100 text-blue-800' };
default:
return null;
}
};
const AlertCard: React.FC<AlertCardProps> = ({ alert, onAction }) => {
const config = getSeverityConfig(alert.severity);
const Icon = getAlertIcon(alert.type);
const urgencyInfo = getUrgencyLabel(alert.urgency);
const handleAction = () => {
onAction?.(alert.id, 'primary_action');
};
return (
<div className={`${config.bgColor} ${config.borderColor} border rounded-lg p-4 shadow-sm`}>
<div className="flex items-start space-x-3">
<div className={`${config.iconColor} mt-0.5`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className={`font-medium ${config.textColor}`}>
{alert.product}
</h4>
{urgencyInfo && (
<span className={`px-2 py-1 text-xs font-bold rounded ${urgencyInfo.color}`}>
{urgencyInfo.label}
</span>
)}
</div>
<p className={`text-sm mt-1 ${config.textColor.replace('900', '700')}`}>
{alert.message}
</p>
{alert.impact && (
<div className={`text-sm font-medium mt-2 ${config.textColor}`}>
{alert.impact.type === 'revenue' && (
<>Impacto: -{alert.impact.value}{alert.impact.currency || '€'}</>
)}
{alert.impact.type === 'units' && (
<>Unidades afectadas: {alert.impact.value}</>
)}
{alert.impact.type === 'percentage' && (
<>Reducción estimada: {alert.impact.value}%</>
)}
</div>
)}
</div>
</div>
<div className="mt-3">
<button
onClick={handleAction}
className={`px-3 py-2 text-sm font-medium rounded transition-colors ${config.actionColor}`}
>
{alert.action}
</button>
</div>
</div>
</div>
</div>
);
};
export default AlertCard;

View File

@@ -0,0 +1,408 @@
import { useState } from 'react';
import { BarChart3, TrendingUp, TrendingDown, Award, Target, Eye, EyeOff } from 'lucide-react';
interface BenchmarkMetric {
id: string;
name: string;
yourValue: number;
industryAverage: number;
topPerformers: number;
unit: string;
description: string;
category: 'efficiency' | 'revenue' | 'waste' | 'customer' | 'quality';
trend: 'improving' | 'declining' | 'stable';
percentile: number; // Your position (0-100)
insights: string[];
}
interface CompetitiveBenchmarksProps {
metrics?: BenchmarkMetric[];
location?: string; // e.g., "Madrid Centro"
showSensitiveData?: boolean;
className?: string;
}
const CompetitiveBenchmarks: React.FC<CompetitiveBenchmarksProps> = ({
metrics: propMetrics,
location = "Madrid Centro",
showSensitiveData = true,
className = ''
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [showDetails, setShowDetails] = useState<boolean>(showSensitiveData);
// Sample benchmark data (anonymized)
const defaultMetrics: BenchmarkMetric[] = [
{
id: 'forecast_accuracy',
name: 'Precisión de Predicciones',
yourValue: 87.2,
industryAverage: 72.5,
topPerformers: 94.1,
unit: '%',
description: 'Qué tan precisas son las predicciones vs. ventas reales',
category: 'quality',
trend: 'improving',
percentile: 85,
insights: [
'Superioridad del 15% vs. promedio de la industria',
'Solo 7 puntos por debajo de los mejores del sector',
'Mejora consistente en los últimos 3 meses'
]
},
{
id: 'waste_percentage',
name: 'Porcentaje de Desperdicio',
yourValue: 8.3,
industryAverage: 12.7,
topPerformers: 4.2,
unit: '%',
description: 'Productos no vendidos como % del total producido',
category: 'waste',
trend: 'improving',
percentile: 78,
insights: [
'35% menos desperdicio que el promedio',
'Oportunidad: reducir 4 puntos más para llegar al top',
'Ahorro de ~€230/mes vs. promedio de la industria'
]
},
{
id: 'revenue_per_sqm',
name: 'Ingresos por m²',
yourValue: 2847,
industryAverage: 2134,
topPerformers: 4521,
unit: '€/mes',
description: 'Ingresos mensuales por metro cuadrado de local',
category: 'revenue',
trend: 'stable',
percentile: 73,
insights: [
'33% más eficiente en generación de ingresos',
'Potencial de crecimiento: +59% para alcanzar el top',
'Excelente aprovechamiento del espacio'
]
},
{
id: 'customer_retention',
name: 'Retención de Clientes',
yourValue: 68,
industryAverage: 61,
topPerformers: 84,
unit: '%',
description: 'Clientes que regresan al menos una vez por semana',
category: 'customer',
trend: 'improving',
percentile: 67,
insights: [
'11% mejor retención que la competencia',
'Oportunidad: programas de fidelización podrían sumar 16 puntos',
'Base de clientes sólida y leal'
]
},
{
id: 'production_efficiency',
name: 'Eficiencia de Producción',
yourValue: 1.8,
industryAverage: 2.3,
topPerformers: 1.2,
unit: 'h/100 unidades',
description: 'Tiempo promedio para producir 100 unidades',
category: 'efficiency',
trend: 'improving',
percentile: 71,
insights: [
'22% más rápido que el promedio',
'Excelente optimización de procesos',
'Margen para mejora: -33% para ser top performer'
]
},
{
id: 'profit_margin',
name: 'Margen de Ganancia',
yourValue: 32.5,
industryAverage: 28.1,
topPerformers: 41.7,
unit: '%',
description: 'Ganancia neta como % de los ingresos totales',
category: 'revenue',
trend: 'stable',
percentile: 69,
insights: [
'16% más rentable que la competencia',
'Sólida gestión de costos',
'Oportunidad: optimizar ingredientes premium'
]
}
];
const metrics = propMetrics || defaultMetrics;
const categories = [
{ id: 'all', name: 'Todas', count: metrics.length },
{ id: 'revenue', name: 'Ingresos', count: metrics.filter(m => m.category === 'revenue').length },
{ id: 'efficiency', name: 'Eficiencia', count: metrics.filter(m => m.category === 'efficiency').length },
{ id: 'waste', name: 'Desperdicio', count: metrics.filter(m => m.category === 'waste').length },
{ id: 'customer', name: 'Clientes', count: metrics.filter(m => m.category === 'customer').length },
{ id: 'quality', name: 'Calidad', count: metrics.filter(m => m.category === 'quality').length }
];
const filteredMetrics = metrics.filter(metric =>
selectedCategory === 'all' || metric.category === selectedCategory
);
const getPerformanceLevel = (percentile: number) => {
if (percentile >= 90) return { label: 'Excelente', color: 'text-green-600', bg: 'bg-green-50' };
if (percentile >= 75) return { label: 'Bueno', color: 'text-blue-600', bg: 'bg-blue-50' };
if (percentile >= 50) return { label: 'Promedio', color: 'text-yellow-600', bg: 'bg-yellow-50' };
return { label: 'Mejora Necesaria', color: 'text-red-600', bg: 'bg-red-50' };
};
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'improving': return <TrendingUp className="h-4 w-4 text-green-600" />;
case 'declining': return <TrendingDown className="h-4 w-4 text-red-600" />;
default: return <div className="w-4 h-4 bg-gray-400 rounded-full"></div>;
}
};
const getComparisonPercentage = (yourValue: number, compareValue: number, isLowerBetter = false) => {
const diff = isLowerBetter
? ((compareValue - yourValue) / compareValue) * 100
: ((yourValue - compareValue) / compareValue) * 100;
return {
value: Math.abs(diff),
isPositive: diff > 0
};
};
const isLowerBetter = (metricId: string) => {
return ['waste_percentage', 'production_efficiency'].includes(metricId);
};
const averagePercentile = Math.round(metrics.reduce((sum, m) => sum + m.percentile, 0) / metrics.length);
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<BarChart3 className="h-6 w-6 text-indigo-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Benchmarks Competitivos
</h3>
<p className="text-sm text-gray-600">
Comparación anónima con panaderías similares en {location}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
{/* Overall Score */}
<div className="text-right">
<div className="text-2xl font-bold text-indigo-600">
{averagePercentile}
</div>
<div className="text-xs text-gray-600">Percentil General</div>
</div>
{/* Toggle Details */}
<button
onClick={() => setShowDetails(!showDetails)}
className="p-2 rounded-lg hover:bg-gray-100 text-gray-600"
title={showDetails ? "Ocultar detalles" : "Mostrar detalles"}
>
{showDetails ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
{/* Performance Summary */}
<div className="mt-4 grid grid-cols-3 gap-4">
<div className="bg-green-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-green-600">
{metrics.filter(m => m.percentile >= 75).length}
</div>
<div className="text-xs text-green-700">Métricas Top 25%</div>
</div>
<div className="bg-blue-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-blue-600">
{metrics.filter(m => m.trend === 'improving').length}
</div>
<div className="text-xs text-blue-700">En Mejora</div>
</div>
<div className="bg-yellow-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-yellow-600">
{metrics.filter(m => m.percentile < 50).length}
</div>
<div className="text-xs text-yellow-700">Áreas de Oportunidad</div>
</div>
</div>
{/* Category Filters */}
<div className="mt-4 flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === category.id
? 'bg-indigo-100 text-indigo-800 border border-indigo-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{category.name}
<span className="ml-1.5 text-xs bg-white rounded-full px-1.5 py-0.5">
{category.count}
</span>
</button>
))}
</div>
</div>
{/* Metrics List */}
<div className="divide-y divide-gray-100">
{filteredMetrics.map(metric => {
const performance = getPerformanceLevel(metric.percentile);
const vsAverage = getComparisonPercentage(
metric.yourValue,
metric.industryAverage,
isLowerBetter(metric.id)
);
const vsTop = getComparisonPercentage(
metric.yourValue,
metric.topPerformers,
isLowerBetter(metric.id)
);
return (
<div key={metric.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-3">
<h4 className="text-lg font-medium text-gray-900">
{metric.name}
</h4>
{getTrendIcon(metric.trend)}
<span className={`px-2 py-1 text-xs rounded-full ${performance.bg} ${performance.color}`}>
{performance.label}
</span>
</div>
<p className="text-sm text-gray-600 mt-1">{metric.description}</p>
</div>
<div className="text-right ml-4">
<div className="text-2xl font-bold text-gray-900">
{metric.yourValue.toLocaleString('es-ES')}<span className="text-sm text-gray-500">{metric.unit}</span>
</div>
<div className="text-sm text-gray-600">Tu Resultado</div>
</div>
</div>
{/* Comparison Bars */}
<div className="space-y-3">
{/* Your Performance */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium text-gray-900">Tu Rendimiento</span>
<span className="text-sm text-indigo-600 font-medium">Percentil {metric.percentile}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-indigo-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${metric.percentile}%` }}
></div>
</div>
</div>
{/* Industry Average */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600">Promedio Industria</span>
<span className="text-sm text-gray-600">
{metric.industryAverage.toLocaleString('es-ES')}{metric.unit}
<span className={`ml-2 ${vsAverage.isPositive ? 'text-green-600' : 'text-red-600'}`}>
({vsAverage.isPositive ? '+' : '-'}{vsAverage.value.toFixed(1)}%)
</span>
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-gray-400 h-1.5 rounded-full"
style={{ width: '50%' }}
></div>
</div>
</div>
{/* Top Performers */}
<div className="relative">
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600 flex items-center">
<Award className="h-3 w-3 mr-1 text-yellow-500" />
Top Performers
</span>
<span className="text-sm text-gray-600">
{metric.topPerformers.toLocaleString('es-ES')}{metric.unit}
<span className={`ml-2 ${vsTop.isPositive ? 'text-green-600' : 'text-orange-600'}`}>
({vsTop.isPositive ? '+' : '-'}{vsTop.value.toFixed(1)}%)
</span>
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-yellow-400 h-1.5 rounded-full"
style={{ width: '90%' }}
></div>
</div>
</div>
</div>
{/* Insights */}
{showDetails && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<h5 className="text-sm font-medium text-gray-700 mb-2 flex items-center">
<Target className="h-4 w-4 mr-2 text-indigo-600" />
Insights Clave:
</h5>
<ul className="space-y-1">
{metric.insights.map((insight, index) => (
<li key={index} className="text-sm text-gray-600 flex items-start">
<div className="w-1.5 h-1.5 bg-indigo-400 rounded-full mt-2 mr-2 flex-shrink-0"></div>
{insight}
</li>
))}
</ul>
</div>
)}
</div>
);
})}
</div>
{filteredMetrics.length === 0 && (
<div className="p-8 text-center">
<BarChart3 className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay métricas disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Los benchmarks aparecerán cuando haya suficientes datos
</p>
</div>
)}
{/* Disclaimer */}
<div className="px-6 pb-4">
<div className="bg-gray-50 rounded-lg p-3">
<div className="text-xs text-gray-600">
<strong>🔒 Privacidad:</strong> Todos los datos están anonimizados.
Solo se comparten métricas agregadas de panaderías similares en tamaño y ubicación.
</div>
</div>
</div>
</div>
);
};
export default CompetitiveBenchmarks;

View File

@@ -0,0 +1,287 @@
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Calendar, TrendingUp, Eye } from 'lucide-react';
export interface DayDemand {
date: string;
demand: number;
isToday?: boolean;
isForecast?: boolean;
products?: Array<{
name: string;
demand: number;
confidence: 'high' | 'medium' | 'low';
}>;
}
export interface WeekData {
weekStart: string;
days: DayDemand[];
}
interface DemandHeatmapProps {
data: WeekData[];
selectedProduct?: string;
onDateClick?: (date: string) => void;
className?: string;
}
const DemandHeatmap: React.FC<DemandHeatmapProps> = ({
data,
selectedProduct,
onDateClick,
className = ''
}) => {
const [currentWeekIndex, setCurrentWeekIndex] = useState(0);
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const maxDemand = Math.max(
...data.flatMap(week => week.days.map(day => day.demand))
);
const getDemandIntensity = (demand: number) => {
const intensity = demand / maxDemand;
if (intensity > 0.8) return 'bg-red-500';
if (intensity > 0.6) return 'bg-orange-500';
if (intensity > 0.4) return 'bg-yellow-500';
if (intensity > 0.2) return 'bg-green-500';
return 'bg-gray-200';
};
const getDemandLabel = (demand: number) => {
const intensity = demand / maxDemand;
if (intensity > 0.8) return 'Muy Alta';
if (intensity > 0.6) return 'Alta';
if (intensity > 0.4) return 'Media';
if (intensity > 0.2) return 'Baja';
return 'Muy Baja';
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.getDate().toString();
};
const formatWeekRange = (weekStart: string) => {
const start = new Date(weekStart);
const end = new Date(start);
end.setDate(end.getDate() + 6);
return `${start.getDate()}-${end.getDate()} ${start.toLocaleDateString('es-ES', { month: 'short' })}`;
};
const handleDateClick = (date: string) => {
setSelectedDate(date);
onDateClick?.(date);
};
const currentWeek = data[currentWeekIndex];
const selectedDay = selectedDate ?
data.flatMap(w => w.days).find(d => d.date === selectedDate) : null;
return (
<div className={`bg-white rounded-xl shadow-soft p-6 ${className}`}>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Calendar className="h-5 w-5 mr-2 text-primary-600" />
Mapa de Calor de Demanda
</h3>
<p className="text-sm text-gray-600 mt-1">
Patrones de demanda visual por día
{selectedProduct && ` - ${selectedProduct}`}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setCurrentWeekIndex(Math.max(0, currentWeekIndex - 1))}
disabled={currentWeekIndex === 0}
className="p-2 rounded-lg bg-gray-100 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-sm font-medium text-gray-700 min-w-[100px] text-center">
{currentWeek ? formatWeekRange(currentWeek.weekStart) : 'Esta Semana'}
</span>
<button
onClick={() => setCurrentWeekIndex(Math.min(data.length - 1, currentWeekIndex + 1))}
disabled={currentWeekIndex === data.length - 1}
className="p-2 rounded-lg bg-gray-100 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
{/* Heatmap Grid */}
<div className="grid grid-cols-7 gap-2 mb-6">
{['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'].map(day => (
<div key={day} className="text-center text-xs font-medium text-gray-600 p-2">
{day}
</div>
))}
{currentWeek?.days.map(day => (
<button
key={day.date}
onClick={() => handleDateClick(day.date)}
className={`
relative p-3 rounded-lg text-white text-sm font-medium
transition-all duration-200 hover:scale-105 hover:shadow-md
${getDemandIntensity(day.demand)}
${selectedDate === day.date ? 'ring-2 ring-primary-600 ring-offset-2' : ''}
${day.isToday ? 'ring-2 ring-blue-400' : ''}
`}
>
<div className="text-center">
<div className="text-lg font-bold">{formatDate(day.date)}</div>
<div className="text-xs opacity-90">{day.demand}</div>
{day.isForecast && (
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-400 rounded-full"></div>
)}
</div>
</button>
))}
</div>
{/* Legend */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">Demanda:</span>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-gray-200 rounded"></div>
<span className="text-xs">Muy Baja</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-green-500 rounded"></div>
<span className="text-xs">Baja</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-yellow-500 rounded"></div>
<span className="text-xs">Media</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-orange-500 rounded"></div>
<span className="text-xs">Alta</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-red-500 rounded"></div>
<span className="text-xs">Muy Alta</span>
</div>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-400 rounded-full mr-1"></div>
Predicción
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-600 rounded-full mr-1"></div>
Hoy
</div>
</div>
</div>
{/* Selected Day Details */}
{selectedDay && (
<div className="border-t border-gray-200 pt-4">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-gray-900 flex items-center">
<Eye className="h-4 w-4 mr-2 text-primary-600" />
{new Date(selectedDay.date).toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long'
})}
{selectedDay.isToday && (
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
Hoy
</span>
)}
</h4>
<div className="mt-2 flex items-center space-x-4">
<div className="flex items-center">
<TrendingUp className="h-4 w-4 text-gray-600 mr-1" />
<span className="text-sm text-gray-600">
Demanda Total: <span className="font-medium">{selectedDay.demand}</span>
</span>
</div>
<span className={`px-2 py-1 text-xs rounded-full ${
getDemandIntensity(selectedDay.demand)
} text-white`}>
{getDemandLabel(selectedDay.demand)}
</span>
</div>
</div>
</div>
{/* Product Breakdown */}
{selectedDay.products && (
<div className="mt-4">
<h5 className="text-sm font-medium text-gray-700 mb-3">Desglose por Producto:</h5>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{selectedDay.products.map((product, index) => (
<div key={index} className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{product.name}</span>
<span className={`px-2 py-1 text-xs rounded ${
product.confidence === 'high' ? 'bg-green-100 text-green-800' :
product.confidence === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{product.confidence === 'high' ? 'Alta' :
product.confidence === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
<div className="text-lg font-bold text-gray-900 mt-1">
{product.demand}
</div>
<div className="text-xs text-gray-600">unidades</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Weekly Summary */}
{currentWeek && (
<div className="mt-6 bg-gray-50 rounded-lg p-4">
<h5 className="text-sm font-medium text-gray-700 mb-2">Resumen Semanal:</h5>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div>
<div className="text-lg font-bold text-gray-900">
{currentWeek.days.reduce((sum, day) => sum + day.demand, 0)}
</div>
<div className="text-xs text-gray-600">Total</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{Math.round(currentWeek.days.reduce((sum, day) => sum + day.demand, 0) / 7)}
</div>
<div className="text-xs text-gray-600">Promedio/día</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{Math.max(...currentWeek.days.map(d => d.demand))}
</div>
<div className="text-xs text-gray-600">Pico</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">
{currentWeek.days.filter(d => d.isForecast).length}
</div>
<div className="text-xs text-gray-600">Predicciones</div>
</div>
</div>
</div>
)}
</div>
);
};
export default DemandHeatmap;

View File

@@ -0,0 +1,207 @@
import React from 'react';
import { Clock, CheckCircle, AlertTriangle, TrendingUp } from 'lucide-react';
export interface ProductionItem {
id: string;
product: string;
quantity: number;
priority: 'high' | 'medium' | 'low';
estimatedTime: number; // minutes
status: 'pending' | 'in_progress' | 'completed';
confidence: number; // 0-1
notes?: string;
}
export interface ProductionTimeSlot {
time: string;
items: ProductionItem[];
totalTime: number;
}
interface ProductionScheduleProps {
schedule: ProductionTimeSlot[];
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
className?: string;
}
const getPriorityConfig = (priority: ProductionItem['priority']) => {
switch (priority) {
case 'high':
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
textColor: 'text-red-800',
label: 'ALTA'
};
case 'medium':
return {
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
textColor: 'text-yellow-800',
label: 'MEDIA'
};
default:
return {
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
textColor: 'text-green-800',
label: 'BAJA'
};
}
};
const getStatusIcon = (status: ProductionItem['status']) => {
switch (status) {
case 'completed':
return <CheckCircle className="h-4 w-4 text-success-600" />;
case 'in_progress':
return <Clock className="h-4 w-4 text-primary-600" />;
default:
return <AlertTriangle className="h-4 w-4 text-gray-400" />;
}
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'text-success-600';
if (confidence >= 0.6) return 'text-warning-600';
return 'text-danger-600';
};
const ProductionItem: React.FC<{
item: ProductionItem;
onUpdateQuantity?: (itemId: string, newQuantity: number) => void;
onUpdateStatus?: (itemId: string, status: ProductionItem['status']) => void;
}> = ({ item, onUpdateQuantity, onUpdateStatus }) => {
const priorityConfig = getPriorityConfig(item.priority);
const handleQuantityChange = (delta: number) => {
const newQuantity = Math.max(0, item.quantity + delta);
onUpdateQuantity?.(item.id, newQuantity);
};
return (
<div className="bg-white border border-gray-200 rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{getStatusIcon(item.status)}
<span className="font-medium text-gray-900">{item.product}</span>
<span className={`px-2 py-1 text-xs font-medium rounded ${priorityConfig.bgColor} ${priorityConfig.borderColor} ${priorityConfig.textColor}`}>
{priorityConfig.label}
</span>
</div>
<div className={`text-sm font-medium ${getConfidenceColor(item.confidence)}`}>
{Math.round(item.confidence * 100)}% confianza
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<button
onClick={() => handleQuantityChange(-5)}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm font-medium"
disabled={item.quantity <= 5}
>
-
</button>
<span className="w-12 text-center font-bold text-lg">{item.quantity}</span>
<button
onClick={() => handleQuantityChange(5)}
className="w-6 h-6 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-sm font-medium"
>
+
</button>
</div>
<span className="text-sm text-gray-600">unidades</span>
</div>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<Clock className="h-4 w-4" />
<span>{item.estimatedTime} min</span>
</div>
</div>
{item.notes && (
<div className="text-sm text-gray-600 bg-gray-50 rounded p-2">
{item.notes}
</div>
)}
<div className="flex space-x-2">
{item.status === 'pending' && (
<button
onClick={() => onUpdateStatus?.(item.id, 'in_progress')}
className="flex-1 px-3 py-2 text-sm font-medium text-primary-700 bg-primary-100 hover:bg-primary-200 rounded transition-colors"
>
Iniciar Producción
</button>
)}
{item.status === 'in_progress' && (
<button
onClick={() => onUpdateStatus?.(item.id, 'completed')}
className="flex-1 px-3 py-2 text-sm font-medium text-success-700 bg-success-100 hover:bg-success-200 rounded transition-colors"
>
Marcar Completado
</button>
)}
{item.status === 'completed' && (
<div className="flex-1 px-3 py-2 text-sm font-medium text-success-700 bg-success-100 rounded text-center">
Completado
</div>
)}
</div>
</div>
);
};
const ProductionSchedule: React.FC<ProductionScheduleProps> = ({
schedule,
onUpdateQuantity,
onUpdateStatus,
className = ''
}) => {
const getTotalItems = (items: ProductionItem[]) => {
return items.reduce((sum, item) => sum + item.quantity, 0);
};
return (
<div className={`space-y-6 ${className}`}>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
Plan de Producción de Hoy
</h3>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<TrendingUp className="h-4 w-4" />
<span>Optimizado por IA</span>
</div>
</div>
{schedule.map((timeSlot, index) => (
<div key={index} className="space-y-4">
<div className="flex items-center space-x-3">
<div className="bg-primary-100 text-primary-800 px-3 py-1 rounded-lg font-medium text-sm">
{timeSlot.time}
</div>
<div className="text-sm text-gray-600">
{getTotalItems(timeSlot.items)} unidades {timeSlot.totalTime} min total
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{timeSlot.items.map((item) => (
<ProductionItem
key={item.id}
item={item}
onUpdateQuantity={onUpdateQuantity}
onUpdateStatus={onUpdateStatus}
/>
))}
</div>
</div>
))}
</div>
);
};
export default ProductionSchedule;

View File

@@ -0,0 +1,373 @@
import { useState } from 'react';
import {
Zap, Plus, Minus, RotateCcw, TrendingUp, ShoppingCart,
Clock, AlertTriangle, Phone, MessageSquare, Calculator,
RefreshCw, Package, Users, Settings, ChevronRight
} from 'lucide-react';
interface QuickAction {
id: string;
title: string;
description: string;
icon: any;
category: 'production' | 'inventory' | 'sales' | 'customer' | 'system';
shortcut?: string;
requiresConfirmation?: boolean;
estimatedTime?: string;
badge?: {
text: string;
color: 'red' | 'yellow' | 'green' | 'blue' | 'purple';
};
}
interface QuickActionsPanelProps {
onActionClick?: (actionId: string) => void;
availableActions?: QuickAction[];
compactMode?: boolean;
showCategories?: boolean;
className?: string;
}
const QuickActionsPanel: React.FC<QuickActionsPanelProps> = ({
onActionClick,
availableActions,
compactMode = false,
showCategories = true,
className = ''
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
const defaultActions: QuickAction[] = [
// Production Actions
{
id: 'increase_production',
title: 'Aumentar Producción',
description: 'Incrementa rápidamente la producción del producto más demandado',
icon: Plus,
category: 'production',
shortcut: 'P + A',
estimatedTime: '2 min',
badge: { text: 'Croissants', color: 'green' }
},
{
id: 'emergency_batch',
title: 'Lote de Emergencia',
description: 'Inicia producción urgente para productos con stock bajo',
icon: AlertTriangle,
category: 'production',
requiresConfirmation: true,
estimatedTime: '45 min',
badge: { text: '3 productos', color: 'red' }
},
{
id: 'adjust_schedule',
title: 'Ajustar Horario',
description: 'Modifica el horario de producción basado en predicciones',
icon: Clock,
category: 'production',
estimatedTime: '1 min'
},
// Inventory Actions
{
id: 'check_stock',
title: 'Revisar Stock',
description: 'Verifica niveles de inventario y productos próximos a agotarse',
icon: Package,
category: 'inventory',
shortcut: 'I + S',
badge: { text: '2 bajos', color: 'yellow' }
},
{
id: 'order_supplies',
title: 'Pedir Suministros',
description: 'Genera orden automática de ingredientes basada en predicciones',
icon: ShoppingCart,
category: 'inventory',
estimatedTime: '3 min',
badge: { text: 'Harina, Huevos', color: 'blue' }
},
{
id: 'waste_report',
title: 'Reportar Desperdicio',
description: 'Registra productos no vendidos para mejorar predicciones',
icon: Minus,
category: 'inventory',
estimatedTime: '2 min'
},
// Sales Actions
{
id: 'price_adjustment',
title: 'Ajustar Precios',
description: 'Modifica precios para productos con baja rotación',
icon: Calculator,
category: 'sales',
requiresConfirmation: true,
badge: { text: 'Magdalenas -10%', color: 'yellow' }
},
{
id: 'promotion_activate',
title: 'Activar Promoción',
description: 'Inicia promoción instantánea para productos específicos',
icon: TrendingUp,
category: 'sales',
estimatedTime: '1 min',
badge: { text: '2x1 Tartas', color: 'green' }
},
// Customer Actions
{
id: 'notify_customers',
title: 'Avisar Clientes',
description: 'Notifica a clientes regulares sobre disponibilidad especial',
icon: MessageSquare,
category: 'customer',
badge: { text: '15 clientes', color: 'blue' }
},
{
id: 'call_supplier',
title: 'Llamar Proveedor',
description: 'Contacto rápido con proveedor principal',
icon: Phone,
category: 'customer',
estimatedTime: '5 min'
},
// System Actions
{
id: 'refresh_predictions',
title: 'Actualizar Predicciones',
description: 'Recalcula predicciones con datos más recientes',
icon: RefreshCw,
category: 'system',
estimatedTime: '30 seg'
},
{
id: 'backup_data',
title: 'Respaldar Datos',
description: 'Crea respaldo de la información del día',
icon: RotateCcw,
category: 'system',
estimatedTime: '1 min'
}
];
const actions = availableActions || defaultActions;
const categories = [
{ id: 'all', name: 'Todas', icon: Zap },
{ id: 'production', name: 'Producción', icon: Package },
{ id: 'inventory', name: 'Inventario', icon: ShoppingCart },
{ id: 'sales', name: 'Ventas', icon: TrendingUp },
{ id: 'customer', name: 'Clientes', icon: Users },
{ id: 'system', name: 'Sistema', icon: Settings }
];
const filteredActions = actions.filter(action =>
selectedCategory === 'all' || action.category === selectedCategory
);
const handleActionClick = async (action: QuickAction) => {
if (action.requiresConfirmation) {
const confirmed = window.confirm(`¿Estás seguro de que quieres ejecutar "${action.title}"?`);
if (!confirmed) return;
}
setActionInProgress(action.id);
// Simulate action execution
await new Promise(resolve => setTimeout(resolve, 1000));
setActionInProgress(null);
onActionClick?.(action.id);
};
const getBadgeColors = (color: string) => {
const colors = {
red: 'bg-red-100 text-red-800',
yellow: 'bg-yellow-100 text-yellow-800',
green: 'bg-green-100 text-green-800',
blue: 'bg-blue-100 text-blue-800',
purple: 'bg-purple-100 text-purple-800'
};
return colors[color as keyof typeof colors] || colors.blue;
};
return (
<div className={`bg-white rounded-xl shadow-soft ${className}`}>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Zap className="h-6 w-6 text-yellow-600 mr-3" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
Acciones Rápidas
</h3>
<p className="text-sm text-gray-600">
Tareas comunes del día a día
</p>
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-500">
{filteredActions.length} acciones disponibles
</div>
<div className="text-xs text-gray-400">
Usa atajos de teclado para mayor velocidad
</div>
</div>
</div>
{/* Category Filters */}
{showCategories && !compactMode && (
<div className="mt-4 flex flex-wrap gap-2">
{categories.map(category => {
const IconComponent = category.icon;
const isSelected = selectedCategory === category.id;
return (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
isSelected
? 'bg-yellow-100 text-yellow-800 border border-yellow-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border border-transparent'
}`}
>
<IconComponent className="h-4 w-4 mr-1.5" />
{category.name}
{category.id !== 'all' && (
<span className="ml-1.5 text-xs bg-white rounded-full px-1.5 py-0.5">
{actions.filter(a => a.category === category.id).length}
</span>
)}
</button>
);
})}
</div>
)}
</div>
{/* Actions Grid */}
<div className="p-6">
<div className={`grid gap-4 ${
compactMode
? 'grid-cols-1'
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
}`}>
{filteredActions.map(action => {
const IconComponent = action.icon;
const isInProgress = actionInProgress === action.id;
return (
<button
key={action.id}
onClick={() => handleActionClick(action)}
disabled={isInProgress}
className={`relative group text-left p-4 border border-gray-200 rounded-lg hover:border-yellow-300 hover:shadow-md transition-all duration-200 ${
isInProgress ? 'opacity-50 cursor-not-allowed' : 'hover:scale-[1.02]'
} ${compactMode ? 'flex items-center' : 'block'}`}
>
{/* Loading overlay */}
{isInProgress && (
<div className="absolute inset-0 bg-white bg-opacity-90 rounded-lg flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-600"></div>
</div>
)}
<div className={`flex ${compactMode ? 'items-center space-x-3' : 'items-start justify-between'}`}>
<div className={`flex ${compactMode ? 'items-center space-x-3' : 'items-start space-x-3'}`}>
{/* Icon */}
<div className="flex-shrink-0 p-2 bg-yellow-50 rounded-lg group-hover:bg-yellow-100 transition-colors">
<IconComponent className="h-5 w-5 text-yellow-600" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900 group-hover:text-yellow-700">
{action.title}
</h4>
{/* Badges */}
<div className="flex items-center space-x-2 ml-2">
{action.badge && (
<span className={`px-2 py-1 text-xs rounded-full ${getBadgeColors(action.badge.color)}`}>
{action.badge.text}
</span>
)}
{action.shortcut && !compactMode && (
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded border font-mono">
{action.shortcut}
</span>
)}
</div>
</div>
{!compactMode && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{action.description}
</p>
)}
{/* Metadata */}
<div className="flex items-center mt-2 space-x-3">
{action.estimatedTime && (
<span className="flex items-center text-xs text-gray-500">
<Clock className="h-3 w-3 mr-1" />
{action.estimatedTime}
</span>
)}
{action.requiresConfirmation && (
<span className="flex items-center text-xs text-orange-600">
<AlertTriangle className="h-3 w-3 mr-1" />
Requiere confirmación
</span>
)}
</div>
</div>
</div>
{/* Arrow indicator */}
{!compactMode && (
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-yellow-600 transition-colors flex-shrink-0" />
)}
</div>
</button>
);
})}
</div>
{filteredActions.length === 0 && (
<div className="text-center py-8">
<Zap className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No hay acciones disponibles</p>
<p className="text-sm text-gray-400 mt-1">
Las acciones aparecerán basadas en el estado actual de tu panadería
</p>
</div>
)}
</div>
{/* Keyboard Shortcuts Help */}
{!compactMode && (
<div className="px-6 pb-4">
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between text-xs text-gray-600">
<span className="font-medium">💡 Tip:</span>
<span>Usa Ctrl + K para búsqueda rápida de acciones</span>
</div>
</div>
</div>
)}
</div>
);
};
export default QuickActionsPanel;

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { Euro, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
export interface RevenueData {
projectedDailyRevenue: number;
lostRevenueFromStockouts: number;
wasteCost: number;
revenueTrend: 'up' | 'down' | 'stable';
trendPercentage: number;
currency: string;
}
interface RevenueMetricsProps {
revenueData: RevenueData;
className?: string;
}
const formatCurrency = (amount: number, currency: string = '€') => {
return `${amount.toFixed(0)}${currency}`;
};
const RevenueMetrics: React.FC<RevenueMetricsProps> = ({ revenueData, className = '' }) => {
const getTrendIcon = () => {
switch (revenueData.revenueTrend) {
case 'up':
return <TrendingUp className="h-4 w-4 text-success-600" />;
case 'down':
return <TrendingDown className="h-4 w-4 text-danger-600" />;
default:
return <TrendingUp className="h-4 w-4 text-gray-600" />;
}
};
const getTrendColor = () => {
switch (revenueData.revenueTrend) {
case 'up':
return 'text-success-600';
case 'down':
return 'text-danger-600';
default:
return 'text-gray-600';
}
};
return (
<div className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}>
{/* Projected Daily Revenue */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-2">
<div className="p-2 bg-success-100 rounded-lg">
<Euro className="h-6 w-6 text-success-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Ingresos Previstos Hoy</p>
<p className="text-2xl font-bold text-gray-900">
{formatCurrency(revenueData.projectedDailyRevenue, revenueData.currency)}
</p>
</div>
</div>
<div className={`flex items-center mt-2 text-sm ${getTrendColor()}`}>
{getTrendIcon()}
<span className="ml-1">
{revenueData.trendPercentage > 0 ? '+' : ''}{revenueData.trendPercentage}% vs ayer
</span>
</div>
</div>
</div>
</div>
{/* Lost Revenue from Stockouts */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center space-x-2">
<div className="p-2 bg-danger-100 rounded-lg">
<AlertCircle className="h-6 w-6 text-danger-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Ventas Perdidas</p>
<p className="text-2xl font-bold text-danger-700">
-{formatCurrency(revenueData.lostRevenueFromStockouts, revenueData.currency)}
</p>
<p className="text-xs text-gray-500 mt-1">Por falta de stock (últimos 7 días)</p>
</div>
</div>
</div>
{/* Waste Cost Tracker */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center space-x-2">
<div className="p-2 bg-warning-100 rounded-lg">
<TrendingDown className="h-6 w-6 text-warning-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-600">Coste Desperdicio</p>
<p className="text-2xl font-bold text-warning-700">
-{formatCurrency(revenueData.wasteCost, revenueData.currency)}
</p>
<p className="text-xs text-gray-500 mt-1">Productos no vendidos (esta semana)</p>
</div>
</div>
</div>
</div>
);
};
export default RevenueMetrics;

View File

@@ -0,0 +1,557 @@
import { useState } from 'react';
import { Play, RotateCcw, TrendingUp, TrendingDown, AlertTriangle, Euro, Calendar } from 'lucide-react';
interface Scenario {
id: string;
name: string;
description: string;
type: 'weather' | 'promotion' | 'event' | 'supply' | 'custom';
icon: any;
parameters: {
[key: string]: {
label: string;
type: 'number' | 'select' | 'boolean';
value: any;
options?: string[];
min?: number;
max?: number;
step?: number;
unit?: string;
};
};
}
interface ScenarioResult {
scenarioId: string;
demandChange: number;
revenueImpact: number;
productImpacts: Array<{
name: string;
demandChange: number;
newDemand: number;
revenueImpact: number;
}>;
recommendations: string[];
confidence: 'high' | 'medium' | 'low';
}
interface WhatIfPlannerProps {
baselineData?: {
totalDemand: number;
totalRevenue: number;
products: Array<{
name: string;
demand: number;
price: number;
}>;
};
onScenarioRun?: (scenario: Scenario, result: ScenarioResult) => void;
className?: string;
}
const WhatIfPlanner: React.FC<WhatIfPlannerProps> = ({
baselineData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Croissants', demand: 45, price: 2.5 },
{ name: 'Pan', demand: 30, price: 1.8 },
{ name: 'Magdalenas', demand: 25, price: 1.2 },
{ name: 'Empanadas', demand: 20, price: 3.2 },
{ name: 'Tartas', demand: 15, price: 12.0 },
]
},
onScenarioRun,
className = ''
}) => {
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
const [scenarioResult, setScenarioResult] = useState<ScenarioResult | null>(null);
const [isRunning, setIsRunning] = useState(false);
const scenarios: Scenario[] = [
{
id: 'rain',
name: 'Día Lluvioso',
description: 'Simula el impacto de un día de lluvia en Madrid',
type: 'weather',
icon: AlertTriangle,
parameters: {
rainIntensity: {
label: 'Intensidad de lluvia',
type: 'select',
value: 'moderate',
options: ['light', 'moderate', 'heavy']
},
temperature: {
label: 'Temperatura (°C)',
type: 'number',
value: 15,
min: 5,
max: 25,
step: 1,
unit: '°C'
}
}
},
{
id: 'promotion',
name: 'Promoción Especial',
description: 'Aplica un descuento y ve el impacto en la demanda',
type: 'promotion',
icon: TrendingUp,
parameters: {
discount: {
label: 'Descuento',
type: 'number',
value: 20,
min: 5,
max: 50,
step: 5,
unit: '%'
},
targetProduct: {
label: 'Producto objetivo',
type: 'select',
value: 'Croissants',
options: baselineData.products.map(p => p.name)
},
duration: {
label: 'Duración (días)',
type: 'number',
value: 3,
min: 1,
max: 7,
step: 1,
unit: 'días'
}
}
},
{
id: 'weekend',
name: 'Fin de Semana',
description: 'Simula la demanda típica de fin de semana',
type: 'event',
icon: Calendar,
parameters: {
dayType: {
label: 'Tipo de día',
type: 'select',
value: 'saturday',
options: ['saturday', 'sunday', 'holiday']
},
weatherGood: {
label: 'Buen tiempo',
type: 'boolean',
value: true
}
}
},
{
id: 'supply_shortage',
name: 'Escasez de Ingredientes',
description: 'Simula falta de ingredientes clave',
type: 'supply',
icon: AlertTriangle,
parameters: {
ingredient: {
label: 'Ingrediente afectado',
type: 'select',
value: 'flour',
options: ['flour', 'butter', 'eggs', 'sugar', 'chocolate']
},
shortage: {
label: 'Nivel de escasez',
type: 'select',
value: 'moderate',
options: ['mild', 'moderate', 'severe']
}
}
}
];
const runScenario = async (scenario: Scenario) => {
setIsRunning(true);
setScenarioResult(null);
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1500));
// Generate realistic scenario results based on parameters
const result = generateScenarioResult(scenario, baselineData);
setScenarioResult(result);
setIsRunning(false);
onScenarioRun?.(scenario, result);
};
const generateScenarioResult = (scenario: Scenario, baseline: typeof baselineData): ScenarioResult => {
let demandChange = 0;
let productImpacts: ScenarioResult['productImpacts'] = [];
let recommendations: string[] = [];
let confidence: 'high' | 'medium' | 'low' = 'medium';
switch (scenario.id) {
case 'rain':
const intensity = scenario.parameters.rainIntensity.value;
demandChange = intensity === 'light' ? -5 : intensity === 'moderate' ? -15 : -25;
confidence = 'high';
recommendations = [
'Reduce la producción de productos para llevar',
'Aumenta café caliente y productos de temporada',
'Prepara promociones para el día siguiente'
];
productImpacts = baseline.products.map(product => {
const change = product.name === 'Croissants' ? demandChange * 1.2 : demandChange;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: Math.max(0, product.demand + Math.round(change * product.demand / 100)),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
case 'promotion':
const discount = scenario.parameters.discount.value;
const targetProduct = scenario.parameters.targetProduct.value;
demandChange = discount * 1.5; // 20% discount = ~30% demand increase
confidence = 'high';
recommendations = [
`Aumenta la producción de ${targetProduct} en un ${Math.round(demandChange)}%`,
'Asegúrate de tener suficientes ingredientes',
'Promociona productos complementarios'
];
productImpacts = baseline.products.map(product => {
const change = product.name === targetProduct ? demandChange : demandChange * 0.3;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: product.demand + Math.round(change * product.demand / 100),
revenueImpact: Math.round((change * product.demand / 100) * product.price * (product.name === targetProduct ? (1 - discount/100) : 1))
};
});
break;
case 'weekend':
const isWeekend = scenario.parameters.dayType.value;
const goodWeather = scenario.parameters.weatherGood.value;
demandChange = (isWeekend === 'saturday' ? 25 : isWeekend === 'sunday' ? 15 : 35) + (goodWeather ? 10 : -5);
confidence = 'high';
recommendations = [
'Aumenta la producción de productos especiales',
'Prepara más variedad para familias',
'Considera abrir más temprano'
];
productImpacts = baseline.products.map(product => {
const multiplier = product.name === 'Tartas' ? 1.5 : product.name === 'Croissants' ? 1.3 : 1.0;
const change = demandChange * multiplier;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: product.demand + Math.round(change * product.demand / 100),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
case 'supply_shortage':
const ingredient = scenario.parameters.ingredient.value;
const shortage = scenario.parameters.shortage.value;
demandChange = shortage === 'mild' ? -10 : shortage === 'moderate' ? -25 : -40;
confidence = 'medium';
recommendations = [
'Busca proveedores alternativos inmediatamente',
'Promociona productos que no requieren este ingrediente',
'Informa a los clientes sobre productos no disponibles'
];
const affectedProducts = getAffectedProducts(ingredient);
productImpacts = baseline.products.map(product => {
const change = affectedProducts.includes(product.name) ? demandChange : 0;
return {
name: product.name,
demandChange: Math.round(change * product.demand / 100),
newDemand: Math.max(0, product.demand + Math.round(change * product.demand / 100)),
revenueImpact: Math.round(change * product.demand * product.price / 100)
};
});
break;
}
const totalRevenueImpact = productImpacts.reduce((sum, impact) => sum + impact.revenueImpact, 0);
return {
scenarioId: scenario.id,
demandChange,
revenueImpact: totalRevenueImpact,
productImpacts,
recommendations,
confidence
};
};
const getAffectedProducts = (ingredient: string): string[] => {
const ingredientMap: Record<string, string[]> = {
flour: ['Pan', 'Croissants', 'Magdalenas', 'Tartas'],
butter: ['Croissants', 'Tartas', 'Magdalenas'],
eggs: ['Magdalenas', 'Tartas'],
sugar: ['Magdalenas', 'Tartas'],
chocolate: ['Tartas']
};
return ingredientMap[ingredient] || [];
};
const updateParameter = (scenarioId: string, paramKey: string, value: any) => {
// This would update the scenario parameters in a real implementation
console.log('Updating parameter:', scenarioId, paramKey, value);
};
const resetScenario = () => {
setSelectedScenario(null);
setScenarioResult(null);
};
const selectedScenarioData = scenarios.find(s => s.id === selectedScenario);
return (
<div className={`bg-white rounded-xl shadow-soft p-6 ${className}`}>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Play className="h-5 w-5 mr-2 text-primary-600" />
Simulador de Escenarios
</h3>
<p className="text-sm text-gray-600 mt-1">
Simula diferentes situaciones y ve el impacto en tu negocio
</p>
</div>
{selectedScenario && (
<button
onClick={resetScenario}
className="flex items-center px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<RotateCcw className="h-4 w-4 mr-1" />
Reiniciar
</button>
)}
</div>
{!selectedScenario ? (
// Scenario Selection
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{scenarios.map(scenario => {
const IconComponent = scenario.icon;
return (
<button
key={scenario.id}
onClick={() => setSelectedScenario(scenario.id)}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-primary-300 hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-start">
<div className="flex-shrink-0 p-2 bg-primary-50 rounded-lg group-hover:bg-primary-100 transition-colors">
<IconComponent className="h-5 w-5 text-primary-600" />
</div>
<div className="ml-3 flex-1">
<h4 className="font-medium text-gray-900 group-hover:text-primary-700">
{scenario.name}
</h4>
<p className="text-sm text-gray-600 mt-1">
{scenario.description}
</p>
</div>
</div>
</button>
);
})}
</div>
) : (
// Selected Scenario Configuration
<div className="space-y-6">
{selectedScenarioData && (
<>
{/* Scenario Header */}
<div className="flex items-center p-4 bg-primary-50 rounded-lg">
<selectedScenarioData.icon className="h-6 w-6 text-primary-600 mr-3" />
<div>
<h4 className="font-medium text-primary-900">{selectedScenarioData.name}</h4>
<p className="text-sm text-primary-700 mt-1">{selectedScenarioData.description}</p>
</div>
</div>
{/* Parameters */}
<div>
<h5 className="text-sm font-medium text-gray-700 mb-3">Parámetros del Escenario:</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(selectedScenarioData.parameters).map(([key, param]) => (
<div key={key} className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
{param.label}
</label>
{param.type === 'select' ? (
<select
value={param.value}
onChange={(e) => updateParameter(selectedScenarioData.id, key, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
{param.options?.map(option => (
<option key={option} value={option}>
{option === 'light' ? 'Ligera' :
option === 'moderate' ? 'Moderada' :
option === 'heavy' ? 'Intensa' :
option === 'saturday' ? 'Sábado' :
option === 'sunday' ? 'Domingo' :
option === 'holiday' ? 'Festivo' :
option === 'mild' ? 'Leve' :
option === 'severe' ? 'Severa' :
option === 'flour' ? 'Harina' :
option === 'butter' ? 'Mantequilla' :
option === 'eggs' ? 'Huevos' :
option === 'sugar' ? 'Azúcar' :
option === 'chocolate' ? 'Chocolate' :
option}
</option>
))}
</select>
) : param.type === 'number' ? (
<div className="flex items-center">
<input
type="number"
value={param.value}
min={param.min}
max={param.max}
step={param.step}
onChange={(e) => updateParameter(selectedScenarioData.id, key, Number(e.target.value))}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
{param.unit && (
<span className="ml-2 text-sm text-gray-600">{param.unit}</span>
)}
</div>
) : param.type === 'boolean' ? (
<label className="flex items-center">
<input
type="checkbox"
checked={param.value}
onChange={(e) => updateParameter(selectedScenarioData.id, key, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-600">
{param.value ? 'Sí' : 'No'}
</span>
</label>
) : null}
</div>
))}
</div>
</div>
{/* Run Scenario Button */}
<button
onClick={() => runScenario(selectedScenarioData)}
disabled={isRunning}
className="w-full flex items-center justify-center px-4 py-3 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRunning ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Simulando...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Ejecutar Simulación
</>
)}
</button>
{/* Results */}
{scenarioResult && (
<div className="border-t border-gray-200 pt-6 space-y-6">
<div>
<h5 className="text-lg font-medium text-gray-900 mb-4">Resultados de la Simulación</h5>
{/* Overall Impact */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Cambio en Demanda</span>
<span className={`text-lg font-bold ${scenarioResult.demandChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{scenarioResult.demandChange >= 0 ? '+' : ''}{scenarioResult.demandChange}%
</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Impacto en Ingresos</span>
<span className={`text-lg font-bold flex items-center ${scenarioResult.revenueImpact >= 0 ? 'text-green-600' : 'text-red-600'}`}>
<Euro className="h-4 w-4 mr-1" />
{scenarioResult.revenueImpact >= 0 ? '+' : ''}{scenarioResult.revenueImpact}
</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Confianza</span>
<span className={`px-2 py-1 text-xs rounded-full ${
scenarioResult.confidence === 'high' ? 'bg-green-100 text-green-800' :
scenarioResult.confidence === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{scenarioResult.confidence === 'high' ? 'Alta' :
scenarioResult.confidence === 'medium' ? 'Media' : 'Baja'}
</span>
</div>
</div>
</div>
{/* Product Impact */}
<div className="mb-6">
<h6 className="text-sm font-medium text-gray-700 mb-3">Impacto por Producto:</h6>
<div className="space-y-2">
{scenarioResult.productImpacts.map((impact, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center">
<span className="font-medium text-gray-900">{impact.name}</span>
<span className="ml-2 text-sm text-gray-600">
{baselineData.products.find(p => p.name === impact.name)?.demand} {impact.newDemand}
</span>
</div>
<div className="flex items-center space-x-3">
<span className={`text-sm font-medium ${impact.demandChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{impact.demandChange >= 0 ? '+' : ''}{impact.demandChange}
</span>
<span className={`text-sm font-medium flex items-center ${impact.revenueImpact >= 0 ? 'text-green-600' : 'text-red-600'}`}>
<Euro className="h-3 w-3 mr-1" />
{impact.revenueImpact >= 0 ? '+' : ''}{impact.revenueImpact}
</span>
</div>
</div>
))}
</div>
</div>
{/* Recommendations */}
<div>
<h6 className="text-sm font-medium text-gray-700 mb-3">Recomendaciones:</h6>
<ul className="space-y-2">
{scenarioResult.recommendations.map((rec, index) => (
<li key={index} className="flex items-start">
<div className="flex-shrink-0 w-2 h-2 bg-primary-600 rounded-full mt-2 mr-3"></div>
<span className="text-sm text-gray-700">{rec}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)}
</>
)}
</div>
)}
</div>
);
};
export default WhatIfPlanner;

View File

@@ -229,14 +229,16 @@ export const useDashboard = () => {
// Load data on mount and when tenant changes
useEffect(() => {
loadDashboardData();
}, [loadDashboardData]);
if (tenantId) {
loadDashboardData(tenantId);
}
}, [loadDashboardData, tenantId]);
return {
...dashboardData,
isLoading: isLoading || dataLoading || forecastLoading,
error: error || dataError || forecastError,
reload: loadDashboardData,
reload: () => tenantId ? loadDashboardData(tenantId) : Promise.resolve(),
clearError: () => setError(null)
};
};

View File

@@ -0,0 +1,286 @@
// Real API hook for Order Suggestions using backend data
import { useState, useCallback, useEffect } from 'react';
import { useData, useForecast } from '../api';
import { useTenantId } from './useTenantId';
import type { DailyOrderItem, WeeklyOrderItem } from '../components/simple/OrderSuggestions';
// Product price mapping that could come from backend
const PRODUCT_PRICES: Record<string, number> = {
'Pan de Molde': 1.80,
'Baguettes': 2.80,
'Croissants': 2.50,
'Magdalenas': 2.40,
'Café en Grano': 17.50, // per kg
'Leche Entera': 0.95, // per liter
'Mantequilla': 4.20, // per kg
'Vasos de Café': 0.08, // per unit
'Servilletas': 0.125, // per pack
'Bolsas papel': 0.12, // per unit
};
// Suppliers mapping
const SUPPLIERS: Record<string, string> = {
'Pan de Molde': 'Panadería Central Madrid',
'Baguettes': 'Panadería Central Madrid',
'Croissants': 'Panadería Central Madrid',
'Magdalenas': 'Panadería Central Madrid',
'Café en Grano': 'Cafés Premium',
'Leche Entera': 'Lácteos Frescos SA',
'Mantequilla': 'Lácteos Frescos SA',
'Vasos de Café': 'Suministros Hostelería',
'Servilletas': 'Suministros Hostelería',
'Bolsas papel': 'Distribuciones Madrid',
};
export const useOrderSuggestions = () => {
const [dailyOrders, setDailyOrders] = useState<DailyOrderItem[]>([]);
const [weeklyOrders, setWeeklyOrders] = useState<WeeklyOrderItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { tenantId } = useTenantId();
const {
getProductsList,
getSalesAnalytics,
getDashboardStats,
getCurrentWeather
} = useData();
const {
createSingleForecast,
getQuickForecasts,
getForecastAlerts
} = useForecast();
// Generate daily order suggestions based on real forecast data
const generateDailyOrderSuggestions = useCallback(async (): Promise<DailyOrderItem[]> => {
if (!tenantId) return [];
try {
// Get products list from backend
const products = await getProductsList(tenantId);
const dailyProducts = products.filter(p =>
['Pan de Molde', 'Baguettes', 'Croissants', 'Magdalenas'].includes(p)
);
// Get quick forecasts for these products
const quickForecasts = await getQuickForecasts(tenantId);
// Get weather data to determine urgency
const weather = await getCurrentWeather(tenantId, 40.4168, -3.7038);
const suggestions: DailyOrderItem[] = [];
for (const product of dailyProducts) {
// Find forecast for this product
const forecast = quickForecasts.find(f => f.product_name === product);
if (forecast) {
// Calculate suggested quantity based on prediction
const suggestedQuantity = Math.max(forecast.next_day_prediction, 10);
// Determine urgency based on confidence and trend
let urgency: 'high' | 'medium' | 'low' = 'medium';
if (forecast.confidence_score > 0.9 && forecast.trend_direction === 'up') {
urgency = 'high';
} else if (forecast.confidence_score < 0.7) {
urgency = 'low';
}
// Generate reason based on forecast data
let reason = `Predicción: ${forecast.next_day_prediction} unidades`;
if (forecast.trend_direction === 'up') {
reason += ' (tendencia al alza)';
}
if (weather && weather.precipitation > 0) {
reason += ', lluvia prevista';
urgency = urgency === 'low' ? 'medium' : 'high';
}
const orderItem: DailyOrderItem = {
id: `daily-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity: Math.round(suggestedQuantity),
currentQuantity: Math.round(suggestedQuantity * 0.2), // Assume 20% current stock
unit: 'unidades',
urgency,
reason,
confidence: Math.round(forecast.confidence_score * 100),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 2.5) * 100) / 100,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
};
suggestions.push(orderItem);
}
}
return suggestions;
} catch (error) {
console.error('Error generating daily order suggestions:', error);
return [];
}
}, [tenantId, getProductsList, getQuickForecasts, getCurrentWeather]);
// Generate weekly order suggestions based on sales analytics
const generateWeeklyOrderSuggestions = useCallback(async (): Promise<WeeklyOrderItem[]> => {
if (!tenantId) return [];
try {
// Get sales analytics for the past month
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const analytics = await getSalesAnalytics(tenantId, startDate, endDate);
// Weekly products (ingredients and supplies)
const weeklyProducts = [
'Café en Grano',
'Leche Entera',
'Mantequilla',
'Vasos de Café',
'Servilletas',
'Bolsas papel'
];
const suggestions: WeeklyOrderItem[] = [];
for (const product of weeklyProducts) {
// Calculate weekly consumption based on analytics
const weeklyConsumption = calculateWeeklyConsumption(product, analytics);
const currentStock = Math.round(weeklyConsumption * (0.3 + Math.random() * 0.4)); // Random stock between 30-70% of weekly need
const stockDays = Math.max(1, Math.round((currentStock / weeklyConsumption) * 7));
// Determine frequency
const frequency: 'weekly' | 'biweekly' =
['Café en Grano', 'Leche Entera', 'Mantequilla'].includes(product) ? 'weekly' : 'biweekly';
const suggestedQuantity = frequency === 'weekly' ?
Math.round(weeklyConsumption * 1.1) : // 10% buffer for weekly
Math.round(weeklyConsumption * 2.2); // 2 weeks + 10% buffer
const orderItem: WeeklyOrderItem = {
id: `weekly-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity,
currentStock,
unit: getProductUnit(product),
frequency,
nextOrderDate: getNextOrderDate(frequency, stockDays),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 1.0) * 100) / 100,
stockDays,
confidence: stockDays <= 2 ? 95 : stockDays <= 5 ? 85 : 75
};
suggestions.push(orderItem);
}
return suggestions.sort((a, b) => a.stockDays - b.stockDays); // Sort by urgency
} catch (error) {
console.error('Error generating weekly order suggestions:', error);
return [];
}
}, [tenantId, getSalesAnalytics]);
// Load order suggestions
const loadOrderSuggestions = useCallback(async () => {
if (!tenantId) return;
setIsLoading(true);
setError(null);
try {
const [daily, weekly] = await Promise.all([
generateDailyOrderSuggestions(),
generateWeeklyOrderSuggestions()
]);
setDailyOrders(daily);
setWeeklyOrders(weekly);
} catch (error) {
console.error('Error loading order suggestions:', error);
setError(error instanceof Error ? error.message : 'Failed to load order suggestions');
} finally {
setIsLoading(false);
}
}, [tenantId, generateDailyOrderSuggestions, generateWeeklyOrderSuggestions]);
// Load on mount and when tenant changes
useEffect(() => {
loadOrderSuggestions();
}, [loadOrderSuggestions]);
return {
dailyOrders,
weeklyOrders,
isLoading,
error,
reload: loadOrderSuggestions,
clearError: () => setError(null),
};
};
// Helper functions
function getProductEmoji(product: string): string {
const emojiMap: Record<string, string> = {
'Pan de Molde': '🍞',
'Baguettes': '🥖',
'Croissants': '🥐',
'Magdalenas': '🧁',
'Café en Grano': '☕',
'Leche Entera': '🥛',
'Mantequilla': '🧈',
'Vasos de Café': '🥤',
'Servilletas': '🧻',
'Bolsas papel': '🛍️'
};
return emojiMap[product] || '📦';
}
function getProductUnit(product: string): string {
const unitMap: Record<string, string> = {
'Pan de Molde': 'unidades',
'Baguettes': 'unidades',
'Croissants': 'unidades',
'Magdalenas': 'unidades',
'Café en Grano': 'kg',
'Leche Entera': 'litros',
'Mantequilla': 'kg',
'Vasos de Café': 'unidades',
'Servilletas': 'paquetes',
'Bolsas papel': 'unidades'
};
return unitMap[product] || 'unidades';
}
function calculateWeeklyConsumption(product: string, analytics: any): number {
// This would ideally come from sales analytics
// For now, use realistic estimates based on product type
const weeklyEstimates: Record<string, number> = {
'Café en Grano': 5, // 5kg per week
'Leche Entera': 25, // 25L per week
'Mantequilla': 3, // 3kg per week
'Vasos de Café': 500, // 500 cups per week
'Servilletas': 10, // 10 packs per week
'Bolsas papel': 200 // 200 bags per week
};
return weeklyEstimates[product] || 10;
}
function getNextOrderDate(frequency: 'weekly' | 'biweekly', stockDays: number): string {
const today = new Date();
let daysToAdd = frequency === 'weekly' ? 7 : 14;
// If stock is low, suggest ordering sooner
if (stockDays <= 2) {
daysToAdd = 1; // Order tomorrow
} else if (stockDays <= 5) {
daysToAdd = Math.min(daysToAdd, 3); // Order within 3 days
}
const nextDate = new Date(today.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
return nextDate.toISOString().split('T')[0];
}

View File

@@ -0,0 +1,162 @@
// Real API hook for Critical Alerts using backend forecast alerts
import { useState, useCallback, useEffect } from 'react';
import { useForecast } from '../api';
import { useTenantId } from './useTenantId';
export interface RealAlert {
id: string;
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
action?: string;
time: string;
}
export const useRealAlerts = () => {
const [alerts, setAlerts] = useState<RealAlert[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { tenantId } = useTenantId();
const { getForecastAlerts, acknowledgeForecastAlert } = useForecast();
// Transform backend forecast alerts to frontend alert format
const transformForecastAlert = (alert: any): RealAlert => {
// Map alert types
let type: RealAlert['type'] = 'system';
if (alert.alert_type?.includes('stock') || alert.alert_type?.includes('demand')) {
type = 'stock';
} else if (alert.alert_type?.includes('weather')) {
type = 'weather';
} else if (alert.alert_type?.includes('production')) {
type = 'production';
}
// Map severity
let severity: RealAlert['severity'] = 'medium';
if (alert.severity === 'critical' || alert.severity === 'high') {
severity = 'high';
} else if (alert.severity === 'low') {
severity = 'low';
}
// Generate user-friendly title and description
let title = alert.message;
let description = alert.message;
if (alert.alert_type?.includes('high_demand')) {
title = 'Alta Demanda Prevista';
description = `Se prevé alta demanda. ${alert.message}`;
} else if (alert.alert_type?.includes('low_confidence')) {
title = 'Predicción Incierta';
description = `Baja confianza en predicción. ${alert.message}`;
} else if (alert.alert_type?.includes('stock_risk')) {
title = 'Riesgo de Desabastecimiento';
description = `Posible falta de stock. ${alert.message}`;
}
return {
id: alert.id,
type,
severity,
title,
description,
action: 'Ver detalles',
time: new Date(alert.created_at).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})
};
};
// Load real alerts from backend
const loadAlerts = useCallback(async () => {
if (!tenantId) return;
setIsLoading(true);
setError(null);
try {
// Get forecast alerts from backend
const forecastAlerts = await getForecastAlerts(tenantId);
// Filter only active alerts
const activeAlerts = forecastAlerts.filter(alert => alert.is_active);
// Transform to frontend format
const transformedAlerts = activeAlerts.map(transformForecastAlert);
// Sort by severity and time (most recent first)
transformedAlerts.sort((a, b) => {
// First by severity (high > medium > low)
const severityOrder = { high: 3, medium: 2, low: 1 };
const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
if (severityDiff !== 0) return severityDiff;
// Then by time (most recent first)
return b.time.localeCompare(a.time);
});
setAlerts(transformedAlerts.slice(0, 3)); // Show max 3 alerts in dashboard
} catch (error) {
console.error('Error loading alerts:', error);
setError(error instanceof Error ? error.message : 'Failed to load alerts');
// Fallback to sample alerts based on common scenarios
setAlerts(generateFallbackAlerts());
} finally {
setIsLoading(false);
}
}, [tenantId, getForecastAlerts]);
// Handle alert acknowledgment
const handleAlertAction = useCallback(async (alertId: string) => {
if (!tenantId) return;
try {
await acknowledgeForecastAlert(tenantId, alertId);
// Remove acknowledged alert from local state
setAlerts(prev => prev.filter(alert => alert.id !== alertId));
} catch (error) {
console.error('Error acknowledging alert:', error);
}
}, [tenantId, acknowledgeForecastAlert]);
// Generate fallback alerts when API fails
const generateFallbackAlerts = (): RealAlert[] => {
const now = new Date();
const timeString = now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
return [
{
id: 'fallback-1',
type: 'stock',
severity: 'high',
title: 'Stock Bajo de Croissants',
description: 'Se prevé alta demanda este fin de semana',
action: 'Aumentar producción',
time: timeString
}
];
};
// Load alerts on mount and when tenant changes
useEffect(() => {
loadAlerts();
// Refresh alerts every 5 minutes
const interval = setInterval(loadAlerts, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [loadAlerts]);
return {
alerts,
isLoading,
error,
onAlertAction: handleAlertAction,
reload: loadAlerts,
clearError: () => setError(null),
};
};

View File

@@ -1,360 +1,242 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, Package, AlertTriangle, Cloud, Users } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import React from 'react';
import { useDashboard } from '../../hooks/useDashboard';
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
import { useRealAlerts } from '../../hooks/useRealAlerts';
// Import simplified components
import TodayRevenue from '../../components/simple/TodayRevenue';
import CriticalAlerts from '../../components/simple/CriticalAlerts';
import TodayProduction from '../../components/simple/TodayProduction';
import QuickActions from '../../components/simple/QuickActions';
import QuickOverview from '../../components/simple/QuickOverview';
import OrderSuggestions from '../../components/simple/OrderSuggestions';
// Helper functions
const getConfidenceColor = (confidence: 'high' | 'medium' | 'low') => {
switch (confidence) {
case 'high':
return 'bg-success-100 text-success-800';
case 'medium':
return 'bg-warning-100 text-warning-800';
case 'low':
return 'bg-danger-100 text-danger-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
interface DashboardPageProps {
onNavigateToOrders?: () => void;
onNavigateToReports?: () => void;
onNavigateToProduction?: () => void;
}
const getConfidenceLabel = (confidence: 'high' | 'medium' | 'low') => {
switch (confidence) {
case 'high':
return 'Alta';
case 'medium':
return 'Media';
case 'low':
return 'Baja';
default:
return 'Media';
}
};
const DashboardPage = () => {
const DashboardPage: React.FC<DashboardPageProps> = ({
onNavigateToOrders,
onNavigateToReports,
onNavigateToProduction
}) => {
const {
weather,
todayForecasts,
metrics,
products,
isLoading,
error,
reload
reload,
todayForecasts,
metrics
} = useDashboard();
if (isLoading) {
return <div>Loading dashboard...</div>;
}
// Use real API data for order suggestions
const {
dailyOrders: realDailyOrders,
weeklyOrders: realWeeklyOrders,
isLoading: ordersLoading
} = useOrderSuggestions();
if (error) {
// Use real API data for alerts
const {
alerts: realAlerts,
onAlertAction
} = useRealAlerts();
// Transform forecast data for production component
const mockProduction = todayForecasts.map((forecast, index) => ({
id: `prod-${index}`,
product: forecast.product,
emoji: forecast.product.toLowerCase().includes('croissant') ? '🥐' :
forecast.product.toLowerCase().includes('pan') ? '🍞' :
forecast.product.toLowerCase().includes('magdalena') ? '🧁' : '🥖',
quantity: forecast.predicted,
status: 'pending' as const,
scheduledTime: index < 3 ? '06:00' : '14:00',
confidence: forecast.confidence === 'high' ? 0.9 :
forecast.confidence === 'medium' ? 0.7 : 0.5
}));
// Helper function for greeting
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return 'Buenos días';
if (hour < 18) return 'Buenas tardes';
return 'Buenas noches';
};
if (isLoading) {
return (
<div>
<p>Error: {error}</p>
<button onClick={reload}>Retry</button>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Cargando datos de tu panadería...</p>
</div>
</div>
);
}
// Sample historical data for charts (you can move this to the hook later)
const salesHistory = [
{ date: '2024-10-28', ventas: 145, prediccion: 140 },
{ date: '2024-10-29', ventas: 128, prediccion: 135 },
{ date: '2024-10-30', ventas: 167, prediccion: 160 },
{ date: '2024-10-31', ventas: 143, prediccion: 145 },
{ date: '2024-11-01', ventas: 156, prediccion: 150 },
{ date: '2024-11-02', ventas: 189, prediccion: 185 },
{ date: '2024-11-03', ventas: 134, prediccion: 130 },
];
const topProducts = [
{ name: 'Croissants', quantity: 45, trend: 'up' },
{ name: 'Pan de molde', quantity: 32, trend: 'up' },
{ name: 'Baguettes', quantity: 28, trend: 'down' },
{ name: 'Napolitanas', quantity: 23, trend: 'up' },
{ name: 'Café', quantity: 67, trend: 'up' },
];
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h3 className="text-red-800 font-medium">Error al cargar datos</h3>
<p className="text-red-700 mt-1">{error}</p>
<button
onClick={() => reload()}
className="mt-4 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg transition-colors"
>
Reintentar
</button>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
{/* ¡Hola, {user.fullName?.split(' ')[0] || 'Usuario'}! 👋 */}
Hola
</h1>
<p className="text-gray-600 mt-1">
Aquí tienes un resumen de tu panadería para hoy
</p>
<div className="p-4 md:p-6 space-y-6 bg-gray-50 min-h-screen">
{/* Welcome Header */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
{getGreeting()}! 👋
</h1>
<p className="text-gray-600 mt-1">
{new Date().toLocaleDateString('es-ES', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</p>
</div>
<div className="mt-4 sm:mt-0 flex items-center space-x-4">
{weather && (
<div className="flex items-center text-sm text-gray-600 bg-gray-50 rounded-lg px-4 py-2">
<span className="text-lg mr-2">
{weather.precipitation > 0 ? '🌧️' : weather.temperature > 20 ? '☀️' : '⛅'}
</span>
<span>{weather.temperature}°C</span>
</div>
)}
<div className="text-right">
<div className="text-sm font-medium text-gray-900">Estado del sistema</div>
<div className="text-xs text-green-600 flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
Operativo
</div>
</div>
</div>
</div>
</div>
{/* Critical Section - Always Visible */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Revenue - Most Important */}
<TodayRevenue
currentRevenue={metrics?.totalSales || 287.50}
previousRevenue={256.25}
dailyTarget={350}
/>
{weather && (
<div className="mt-4 sm:mt-0 flex items-center text-sm text-gray-600 bg-white rounded-lg px-4 py-2 shadow-soft">
<Cloud className="h-4 w-4 mr-2" />
<span>{weather.temperature}°C - {weather.description}</span>
</div>
)}
{/* Alerts - Real API Data */}
<CriticalAlerts
alerts={realAlerts}
onAlertClick={onAlertAction}
/>
{/* Quick Actions - Easy Access */}
<QuickActions
onActionClick={(actionId) => {
console.log('Action clicked:', actionId);
// Handle quick actions
switch (actionId) {
case 'view_orders':
onNavigateToOrders?.();
break;
case 'view_sales':
onNavigateToReports?.();
break;
default:
// Handle other actions
break;
}
}}
/>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Ventas de Hoy</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.totalSales ?? 0}</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
+12% vs ayer
</p>
</div>
</div>
</div>
{/* Order Suggestions - Real AI-Powered Recommendations */}
<OrderSuggestions
dailyOrders={realDailyOrders}
weeklyOrders={realWeeklyOrders}
onUpdateQuantity={(orderId, quantity, type) => {
console.log('Update order quantity:', orderId, quantity, type);
// In real implementation, this would update the backend
}}
onCreateOrder={(items, type) => {
console.log('Create order:', type, items);
// Navigate to orders page to complete the order
onNavigateToOrders?.();
}}
onViewDetails={() => {
onNavigateToOrders?.();
}}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-success-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Reducción Desperdicio</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.wasteReduction ?? 0}%</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
Mejorando
</p>
</div>
</div>
</div>
{/* Production Section - Core Operations */}
<TodayProduction
items={mockProduction}
onUpdateQuantity={(itemId: string, quantity: number) => {
console.log('Update quantity:', itemId, quantity);
}}
onUpdateStatus={(itemId: string, status: any) => {
console.log('Update status:', itemId, status);
}}
onViewDetails={() => {
onNavigateToProduction?.();
}}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Users className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Precisión IA</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.accuracy ?? 0}%</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
Excelente
</p>
</div>
</div>
</div>
{/* Quick Overview - Supporting Information */}
<QuickOverview
onNavigateToOrders={onNavigateToOrders}
onNavigateToReports={onNavigateToReports}
/>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-warning-100 rounded-lg">
<AlertTriangle className="h-6 w-6 text-warning-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Roturas Stock</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.stockouts ?? 0}</p>
<p className="text-xs text-success-600 flex items-center mt-1">
<TrendingDown className="h-3 w-3 mr-1" />
Reduciendo
</p>
</div>
</div>
</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sales Chart */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Ventas vs Predicciones (Última Semana)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={salesHistory}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
stroke="#666"
fontSize={12}
tickFormatter={(value) => {
const date = new Date(value);
return `${date.getDate()}/${date.getMonth() + 1}`;
}}
/>
<YAxis stroke="#666" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
labelFormatter={(value) => {
const date = new Date(value);
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
}}
/>
<Line
type="monotone"
dataKey="ventas"
stroke="#f97316"
strokeWidth={3}
name="Ventas Reales"
dot={{ fill: '#f97316', strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="prediccion"
stroke="#64748b"
strokeWidth={2}
strokeDasharray="5 5"
name="Predicción IA"
dot={{ fill: '#64748b', strokeWidth: 2, r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Today's Forecasts */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Predicciones para Hoy
</h3>
<div className="space-y-4">
{todayForecasts.map((forecast, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-900">{forecast.product}</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${getConfidenceColor(forecast.confidence)}`}>
{getConfidenceLabel(forecast.confidence)}
</span>
</div>
<div className="flex items-center mt-1">
<span className="text-xl font-bold text-gray-900 mr-2">
{forecast.predicted}
</span>
<span className={`text-sm flex items-center ${
forecast.change >= 0 ? 'text-success-600' : 'text-danger-600'
}`}>
{forecast.change >= 0 ? (
<TrendingUp className="h-3 w-3 mr-1" />
) : (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{Math.abs(forecast.change)} vs ayer
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bottom Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Productos Más Vendidos (Esta Semana)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={topProducts}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="name"
stroke="#666"
fontSize={12}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis stroke="#666" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
/>
<Bar
dataKey="quantity"
fill="#f97316"
radius={[4, 4, 0, 0]}
name="Cantidad Vendida"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Acciones Rápidas
</h3>
<div className="space-y-3">
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg mr-3">
<TrendingUp className="h-5 w-5 text-primary-600" />
</div>
<div>
<div className="font-medium text-gray-900">Ver Predicciones Detalladas</div>
<div className="text-sm text-gray-500">Analiza las predicciones completas</div>
</div>
</div>
</button>
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg mr-3">
<Package className="h-5 w-5 text-success-600" />
</div>
<div>
<div className="font-medium text-gray-900">Gestionar Pedidos</div>
<div className="text-sm text-gray-500">Revisa y ajusta tus pedidos</div>
</div>
</div>
</button>
<button className="w-full p-4 text-left border border-gray-200 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg mr-3">
<Users className="h-5 w-5 text-blue-600" />
</div>
<div>
<div className="font-medium text-gray-900">Configurar Alertas</div>
<div className="text-sm text-gray-500">Personaliza tus notificaciones</div>
</div>
</div>
</button>
</div>
</div>
</div>
{/* Weather Impact Alert */}
{/* Weather Impact Alert - Context Aware */}
{weather && weather.precipitation > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start">
<Cloud className="h-5 w-5 text-blue-600 mt-0.5 mr-3" />
<span className="text-2xl mr-3">🌧</span>
<div>
<h4 className="font-medium text-blue-900">Impacto del Clima</h4>
<h4 className="font-medium text-blue-900">Impacto del Clima Detectado</h4>
<p className="text-blue-800 text-sm mt-1">
Se esperan precipitaciones hoy. Esto puede reducir el tráfico peatonal en un 20-30%.
Considera ajustar la producción de productos frescos.
Se esperan precipitaciones ({weather.precipitation}mm). Las predicciones se han ajustado
automáticamente considerando una reducción del 15% en el tráfico.
</p>
<div className="mt-2 flex items-center text-xs text-blue-700">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-2"></div>
Producción y pedidos ya optimizados
</div>
</div>
</div>
</div>
)}
{/* Success Message - When Everything is Good */}
{realAlerts.length === 0 && (
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
<div className="text-4xl mb-2">🎉</div>
<h4 className="font-medium text-green-900">¡Todo bajo control!</h4>
<p className="text-green-700 text-sm mt-1">
No hay alertas activas. Tu panadería está funcionando perfectamente.
</p>
</div>
)}
</div>
);
};

View File

@@ -1,5 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock } from 'lucide-react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock, BarChart3, TrendingUp, Euro, Settings } from 'lucide-react';
// Import complex components
import WhatIfPlanner from '../../components/ui/WhatIfPlanner';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
interface Order {
id: string;
@@ -24,7 +28,7 @@ const OrdersPage: React.FC = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showNewOrder, setShowNewOrder] = useState(false);
const [activeTab, setActiveTab] = useState<'all' | 'pending' | 'delivered'>('all');
const [activeTab, setActiveTab] = useState<'orders' | 'analytics' | 'forecasting' | 'suppliers'>('orders');
// Sample orders data
const sampleOrders: Order[] = [
@@ -135,11 +139,47 @@ const OrdersPage: React.FC = () => {
}
};
// Sample data for complex components
const orderDemandHeatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Harina de trigo', demand: 50, confidence: 'high' as const },
{ name: 'Levadura fresca', demand: 2, confidence: 'high' as const },
{ name: 'Mantequilla', demand: 5, confidence: 'medium' as const },
{ name: 'Vasos café', demand: 1000, confidence: 'medium' as const },
]
},
{ date: '2024-11-05', demand: 165, isForecast: true },
{ date: '2024-11-06', demand: 195, isForecast: true },
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const baselineSupplyData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Harina de trigo', demand: 50, price: 0.85 },
{ name: 'Levadura fresca', demand: 2, price: 3.20 },
{ name: 'Mantequilla', demand: 5, price: 4.20 },
{ name: 'Leche entera', demand: 20, price: 0.95 },
{ name: 'Vasos café', demand: 1000, price: 0.08 },
]
};
const filteredOrders = orders.filter(order => {
if (activeTab === 'all') return true;
if (activeTab === 'pending') return order.status === 'pending' || order.status === 'confirmed';
if (activeTab === 'delivered') return order.status === 'delivered';
return true;
if (activeTab === 'orders') return true;
return false;
});
const handleDeleteOrder = (orderId: string) => {
@@ -181,24 +221,31 @@ const OrdersPage: React.FC = () => {
</button>
</div>
{/* Tabs */}
{/* Enhanced Tabs */}
<div className="bg-white rounded-xl shadow-soft p-1">
<div className="flex space-x-1">
{[
{ id: 'all', label: 'Todos', count: orders.length },
{ id: 'pending', label: 'Pendientes', count: orders.filter(o => o.status === 'pending' || o.status === 'confirmed').length },
{ id: 'delivered', label: 'Entregados', count: orders.filter(o => o.status === 'delivered').length }
{ id: 'orders', label: 'Gestión de Pedidos', icon: Package, count: orders.length },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'forecasting', label: 'Simulaciones', icon: TrendingUp },
{ id: 'suppliers', label: 'Proveedores', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-lg transition-all ${
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
{tab.label} ({tab.count})
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
{tab.count && (
<span className="ml-2 px-2 py-1 bg-gray-200 text-gray-700 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
@@ -224,8 +271,11 @@ const OrdersPage: React.FC = () => {
</div>
</div>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Tab Content */}
{activeTab === 'orders' && (
<>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredOrders.map((order) => (
<div key={order.id} className="bg-white rounded-xl shadow-soft hover:shadow-medium transition-shadow">
{/* Order Header */}
@@ -390,6 +440,148 @@ const OrdersPage: React.FC = () => {
</button>
</div>
</div>
</>
)}
{/* Analytics Tab */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={orderDemandHeatmapData}
selectedProduct="Ingredientes"
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Cost Analysis Chart */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Euro className="h-5 w-5 mr-2 text-primary-600" />
Análisis de Costos
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800 font-semibold">Ahorro Mensual</div>
<div className="text-2xl font-bold text-green-900">124.50</div>
<div className="text-sm text-green-700">vs mes anterior</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-blue-800 font-semibold">Gasto Promedio</div>
<div className="text-2xl font-bold text-blue-900">289.95</div>
<div className="text-sm text-blue-700">por pedido</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-purple-800 font-semibold">Eficiencia</div>
<div className="text-2xl font-bold text-purple-900">94.2%</div>
<div className="text-sm text-purple-700">predicción IA</div>
</div>
</div>
<div className="mt-6 h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de costos</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{/* Forecasting/Simulations Tab */}
{activeTab === 'forecasting' && (
<div className="space-y-6">
<WhatIfPlanner
baselineData={baselineSupplyData}
onScenarioRun={(scenario, result) => {
console.log('Scenario run:', scenario, result);
}}
/>
</div>
)}
{/* Suppliers Tab */}
{activeTab === 'suppliers' && (
<div className="space-y-6">
{/* Suppliers Management */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Settings className="h-5 w-5 mr-2 text-primary-600" />
Gestión de Proveedores
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
name: 'Harinas Castellana',
category: 'Ingredientes',
rating: 4.8,
reliability: 98,
nextDelivery: '2024-11-05',
status: 'active'
},
{
name: 'Distribuciones Madrid',
category: 'Consumibles',
rating: 4.5,
reliability: 95,
nextDelivery: '2024-11-04',
status: 'active'
},
{
name: 'Lácteos Frescos SA',
category: 'Ingredientes',
rating: 4.9,
reliability: 99,
nextDelivery: '2024-11-03',
status: 'active'
}
].map((supplier, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{supplier.name}</h4>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
</span>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Categoría:</span> {supplier.category}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Calificación:</span> {supplier.rating}/5
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Confiabilidad:</span> {supplier.reliability}%
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Próxima entrega:</span> {new Date(supplier.nextDelivery).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-4 flex space-x-2">
<button className="flex-1 px-3 py-2 text-sm bg-primary-100 text-primary-700 rounded-lg hover:bg-primary-200 transition-colors">
Editar
</button>
<button className="px-3 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
Contactar
</button>
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors">
<Plus className="h-4 w-4 mr-2" />
Añadir Proveedor
</button>
</div>
</div>
</div>
)}
{/* New Order Modal Placeholder */}
{showNewOrder && (

View File

@@ -0,0 +1,671 @@
import React, { useState, useEffect } from 'react';
import {
Clock, Calendar, ChefHat, TrendingUp, AlertTriangle,
CheckCircle, Settings, Plus, BarChart3, Users,
Timer, Target, Activity, Zap
} from 'lucide-react';
// Import existing complex components
import ProductionSchedule from '../../components/ui/ProductionSchedule';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
import { useDashboard } from '../../hooks/useDashboard';
// Types for production management
interface ProductionMetrics {
efficiency: number;
onTimeCompletion: number;
wastePercentage: number;
energyUsage: number;
staffUtilization: number;
}
interface ProductionBatch {
id: string;
product: string;
batchSize: number;
startTime: string;
endTime: string;
status: 'planned' | 'in_progress' | 'completed' | 'delayed';
assignedStaff: string[];
actualYield: number;
expectedYield: number;
notes?: string;
temperature?: number;
humidity?: number;
}
interface StaffMember {
id: string;
name: string;
role: 'baker' | 'assistant' | 'decorator';
currentTask?: string;
status: 'available' | 'busy' | 'break';
efficiency: number;
}
interface Equipment {
id: string;
name: string;
type: 'oven' | 'mixer' | 'proofer' | 'cooling_rack';
status: 'idle' | 'in_use' | 'maintenance' | 'error';
currentBatch?: string;
temperature?: number;
maintenanceDue?: string;
}
const ProductionPage: React.FC = () => {
const { todayForecasts, metrics, weather, isLoading } = useDashboard();
const [activeTab, setActiveTab] = useState<'schedule' | 'batches' | 'analytics' | 'staff' | 'equipment'>('schedule');
const [productionMetrics, setProductionMetrics] = useState<ProductionMetrics>({
efficiency: 87.5,
onTimeCompletion: 94.2,
wastePercentage: 3.8,
energyUsage: 156.7,
staffUtilization: 78.3
});
// Sample production schedule data
const [productionSchedule, setProductionSchedule] = useState([
{
time: '05:00 AM',
items: [
{
id: 'prod-1',
product: 'Croissants',
quantity: 48,
priority: 'high' as const,
estimatedTime: 180,
status: 'in_progress' as const,
confidence: 0.92,
notes: 'Alta demanda prevista - lote doble'
},
{
id: 'prod-2',
product: 'Pan de molde',
quantity: 35,
priority: 'high' as const,
estimatedTime: 240,
status: 'pending' as const,
confidence: 0.88
}
],
totalTime: 420
},
{
time: '08:00 AM',
items: [
{
id: 'prod-3',
product: 'Baguettes',
quantity: 25,
priority: 'medium' as const,
estimatedTime: 200,
status: 'pending' as const,
confidence: 0.75
},
{
id: 'prod-4',
product: 'Magdalenas',
quantity: 60,
priority: 'medium' as const,
estimatedTime: 120,
status: 'pending' as const,
confidence: 0.82
}
],
totalTime: 320
}
]);
const [productionBatches, setProductionBatches] = useState<ProductionBatch[]>([
{
id: 'batch-1',
product: 'Croissants',
batchSize: 48,
startTime: '05:00',
endTime: '08:00',
status: 'in_progress',
assignedStaff: ['maria-lopez', 'carlos-ruiz'],
actualYield: 45,
expectedYield: 48,
temperature: 180,
humidity: 65,
notes: 'Masa fermentando correctamente'
},
{
id: 'batch-2',
product: 'Pan de molde',
batchSize: 35,
startTime: '06:30',
endTime: '10:30',
status: 'planned',
assignedStaff: ['ana-garcia'],
actualYield: 0,
expectedYield: 35,
notes: 'Esperando finalización de croissants'
}
]);
const [staff, setStaff] = useState<StaffMember[]>([
{
id: 'maria-lopez',
name: 'María López',
role: 'baker',
currentTask: 'Preparando croissants',
status: 'busy',
efficiency: 94.2
},
{
id: 'carlos-ruiz',
name: 'Carlos Ruiz',
role: 'assistant',
currentTask: 'Horneando croissants',
status: 'busy',
efficiency: 87.8
},
{
id: 'ana-garcia',
name: 'Ana García',
role: 'baker',
status: 'available',
efficiency: 91.5
}
]);
const [equipment, setEquipment] = useState<Equipment[]>([
{
id: 'oven-1',
name: 'Horno Principal',
type: 'oven',
status: 'in_use',
currentBatch: 'batch-1',
temperature: 180,
maintenanceDue: '2024-11-15'
},
{
id: 'mixer-1',
name: 'Amasadora Industrial',
type: 'mixer',
status: 'idle',
maintenanceDue: '2024-11-20'
},
{
id: 'proofer-1',
name: 'Fermentadora',
type: 'proofer',
status: 'in_use',
currentBatch: 'batch-2',
temperature: 28,
maintenanceDue: '2024-12-01'
}
]);
// Demand heatmap sample data
const heatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Croissants', demand: 48, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 35, confidence: 'high' as const },
{ name: 'Baguettes', demand: 25, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 32, confidence: 'medium' as const },
]
},
{
date: '2024-11-05',
demand: 165,
isForecast: true,
products: [
{ name: 'Croissants', demand: 42, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 38, confidence: 'medium' as const },
{ name: 'Baguettes', demand: 28, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 28, confidence: 'low' as const },
]
},
{
date: '2024-11-06',
demand: 195,
isForecast: true,
products: [
{ name: 'Croissants', demand: 55, confidence: 'high' as const },
{ name: 'Pan de molde', demand: 40, confidence: 'high' as const },
{ name: 'Baguettes', demand: 32, confidence: 'medium' as const },
{ name: 'Magdalenas', demand: 35, confidence: 'medium' as const },
]
},
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const getStatusColor = (status: string) => {
switch (status) {
case 'planned':
return 'bg-blue-100 text-blue-800';
case 'in_progress':
return 'bg-yellow-100 text-yellow-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'delayed':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getEquipmentStatusColor = (status: Equipment['status']) => {
switch (status) {
case 'idle':
return 'bg-gray-100 text-gray-800';
case 'in_use':
return 'bg-green-100 text-green-800';
case 'maintenance':
return 'bg-yellow-100 text-yellow-800';
case 'error':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6 bg-gray-50 min-h-screen">
{/* Header */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<ChefHat className="h-8 w-8 mr-3 text-primary-600" />
Centro de Producción
</h1>
<p className="text-gray-600 mt-1">
Gestión completa de la producción diaria y planificación inteligente
</p>
</div>
<div className="mt-4 lg:mt-0 flex items-center space-x-4">
<div className="bg-gray-50 rounded-lg px-4 py-2">
<div className="text-sm font-medium text-gray-900">Eficiencia Hoy</div>
<div className="text-2xl font-bold text-primary-600">{productionMetrics.efficiency}%</div>
</div>
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors">
<Plus className="h-5 w-5 mr-2" />
Nuevo Lote
</button>
</div>
</div>
</div>
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Eficiencia</p>
<p className="text-2xl font-bold text-green-600">{productionMetrics.efficiency}%</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<Target className="h-6 w-6 text-green-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-green-600">
<TrendingUp className="h-3 w-3 mr-1" />
+2.3% vs ayer
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">A Tiempo</p>
<p className="text-2xl font-bold text-blue-600">{productionMetrics.onTimeCompletion}%</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<Clock className="h-6 w-6 text-blue-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-blue-600">
<CheckCircle className="h-3 w-3 mr-1" />
Muy bueno
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Desperdicio</p>
<p className="text-2xl font-bold text-orange-600">{productionMetrics.wastePercentage}%</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<AlertTriangle className="h-6 w-6 text-orange-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-orange-600">
<TrendingUp className="h-3 w-3 mr-1" />
-0.5% vs ayer
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Energía</p>
<p className="text-2xl font-bold text-purple-600">{productionMetrics.energyUsage} kW</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<Zap className="h-6 w-6 text-purple-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-purple-600">
<Activity className="h-3 w-3 mr-1" />
Normal
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Personal</p>
<p className="text-2xl font-bold text-indigo-600">{productionMetrics.staffUtilization}%</p>
</div>
<div className="p-3 bg-indigo-100 rounded-lg">
<Users className="h-6 w-6 text-indigo-600" />
</div>
</div>
<div className="mt-2 flex items-center text-xs text-indigo-600">
<Users className="h-3 w-3 mr-1" />
3/4 activos
</div>
</div>
</div>
{/* Tabs Navigation */}
<div className="bg-white rounded-xl shadow-sm p-1">
<div className="flex space-x-1">
{[
{ id: 'schedule', label: 'Programa', icon: Calendar },
{ id: 'batches', label: 'Lotes Activos', icon: Timer },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'staff', label: 'Personal', icon: Users },
{ id: 'equipment', label: 'Equipos', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
</button>
))}
</div>
</div>
{/* Tab Content */}
<div className="space-y-6">
{activeTab === 'schedule' && (
<>
<ProductionSchedule
schedule={productionSchedule}
onUpdateQuantity={(itemId, quantity) => {
setProductionSchedule(prev =>
prev.map(slot => ({
...slot,
items: slot.items.map(item =>
item.id === itemId ? { ...item, quantity } : item
)
}))
);
}}
onUpdateStatus={(itemId, status) => {
setProductionSchedule(prev =>
prev.map(slot => ({
...slot,
items: slot.items.map(item =>
item.id === itemId ? { ...item, status } : item
)
}))
);
}}
/>
</>
)}
{activeTab === 'batches' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{productionBatches.map((batch) => (
<div key={batch.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{batch.product}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(batch.status)}`}>
{batch.status === 'planned' ? 'Planificado' :
batch.status === 'in_progress' ? 'En Progreso' :
batch.status === 'completed' ? 'Completado' : 'Retrasado'}
</span>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Tamaño del Lote</p>
<p className="font-semibold text-gray-900">{batch.batchSize} unidades</p>
</div>
<div>
<p className="text-sm text-gray-600">Rendimiento</p>
<p className="font-semibold text-gray-900">
{batch.actualYield || 0}/{batch.expectedYield}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Inicio</p>
<p className="font-semibold text-gray-900">{batch.startTime}</p>
</div>
<div>
<p className="text-sm text-gray-600">Fin Estimado</p>
<p className="font-semibold text-gray-900">{batch.endTime}</p>
</div>
</div>
{(batch.temperature || batch.humidity) && (
<div className="grid grid-cols-2 gap-4">
{batch.temperature && (
<div>
<p className="text-sm text-gray-600">Temperatura</p>
<p className="font-semibold text-gray-900">{batch.temperature}°C</p>
</div>
)}
{batch.humidity && (
<div>
<p className="text-sm text-gray-600">Humedad</p>
<p className="font-semibold text-gray-900">{batch.humidity}%</p>
</div>
)}
</div>
)}
<div>
<p className="text-sm text-gray-600 mb-2">Personal Asignado</p>
<div className="flex space-x-2">
{batch.assignedStaff.map((staffId) => {
const staffMember = staff.find(s => s.id === staffId);
return (
<span
key={staffId}
className="px-2 py-1 bg-gray-100 text-gray-700 text-sm rounded"
>
{staffMember?.name || staffId}
</span>
);
})}
</div>
</div>
{batch.notes && (
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-sm text-gray-700">{batch.notes}</p>
</div>
)}
</div>
</div>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={heatmapData}
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Production Trends Chart Placeholder */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<BarChart3 className="h-5 w-5 mr-2 text-primary-600" />
Tendencias de Producción
</h3>
<div className="h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de producción</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{activeTab === 'staff' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{staff.map((member) => (
<div key={member.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{member.name}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
member.status === 'available' ? 'bg-green-100 text-green-800' :
member.status === 'busy' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{member.status === 'available' ? 'Disponible' :
member.status === 'busy' ? 'Ocupado' : 'Descanso'}
</span>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Rol</p>
<p className="font-medium text-gray-900 capitalize">{member.role}</p>
</div>
{member.currentTask && (
<div>
<p className="text-sm text-gray-600">Tarea Actual</p>
<p className="font-medium text-gray-900">{member.currentTask}</p>
</div>
)}
<div>
<p className="text-sm text-gray-600">Eficiencia</p>
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full"
style={{ width: `${member.efficiency}%` }}
></div>
</div>
<span className="text-sm font-medium text-gray-900">{member.efficiency}%</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
{activeTab === 'equipment' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{equipment.map((item) => (
<div key={item.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">{item.name}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getEquipmentStatusColor(item.status)}`}>
{item.status === 'idle' ? 'Inactivo' :
item.status === 'in_use' ? 'En Uso' :
item.status === 'maintenance' ? 'Mantenimiento' : 'Error'}
</span>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Tipo</p>
<p className="font-medium text-gray-900 capitalize">{item.type}</p>
</div>
{item.currentBatch && (
<div>
<p className="text-sm text-gray-600">Lote Actual</p>
<p className="font-medium text-gray-900">
{productionBatches.find(b => b.id === item.currentBatch)?.product || item.currentBatch}
</p>
</div>
)}
{item.temperature && (
<div>
<p className="text-sm text-gray-600">Temperatura</p>
<p className="font-medium text-gray-900">{item.temperature}°C</p>
</div>
)}
{item.maintenanceDue && (
<div>
<p className="text-sm text-gray-600">Próximo Mantenimiento</p>
<p className="font-medium text-orange-600">
{new Date(item.maintenanceDue).toLocaleDateString('es-ES')}
</p>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default ProductionPage;

31
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.js"]
}

View File

@@ -1,78 +0,0 @@
# ================================================================
# Kubernetes Deployment: infrastructure/kubernetes/base/forecasting-service.yaml
# ================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: forecasting-service
labels:
app: forecasting-service
spec:
replicas: 2
selector:
matchLabels:
app: forecasting-service
template:
metadata:
labels:
app: forecasting-service
spec:
containers:
- name: forecasting-service
image: bakery-forecasting/forecasting-service:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: forecasting-db-secret
key: database-url
- name: RABBITMQ_URL
valueFrom:
secretKeyRef:
name: rabbitmq-secret
key: url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: redis-secret
key: url
- name: TRAINING_SERVICE_URL
value: "http://training-service:8000"
- name: DATA_SERVICE_URL
value: "http://data-service:8000"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: forecasting-service
labels:
app: forecasting-service
spec:
selector:
app: forecasting-service
ports:
- port: 8000
targetPort: 8000
type: ClusterIP

View File

@@ -1,399 +0,0 @@
# Complete Microservices Project Structure
## 📁 Project Root Structure
```
bakery-forecasting-platform/
├── README.md
├── docker-compose.yml # Development environment
├── docker-compose.prod.yml # Production environment
├── .env.example # Environment variables template
├── .gitignore # Git ignore file
├── docs/ # Documentation
│ ├── architecture/ # Architecture diagrams
│ ├── api/ # API documentation
│ └── deployment/ # Deployment guides
├── scripts/ # Utility scripts
│ ├── setup.sh # Development setup
│ ├── test.sh # Run all tests
│ └── deploy.sh # Deployment script
├── gateway/ # API Gateway Service
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── core/
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── auth.py
│ │ │ └── service_discovery.py
│ │ ├── middleware/
│ │ │ ├── __init__.py
│ │ │ ├── cors.py
│ │ │ ├── auth.py
│ │ │ └── logging.py
│ │ └── routes/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── training.py
│ │ ├── forecasting.py
│ │ └── data.py
│ └── tests/
│ ├── __init__.py
│ ├── test_gateway.py
│ └── test_routes.py
├── services/ # Individual Microservices
│ ├── auth/ # Authentication Service
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── core/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── database.py
│ │ │ │ ├── security.py
│ │ │ │ └── auth.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── users.py
│ │ │ │ └── tokens.py
│ │ │ ├── schemas/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── users.py
│ │ │ │ └── auth.py
│ │ │ ├── services/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── auth_service.py
│ │ │ │ └── user_service.py
│ │ │ └── api/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ └── users.py
│ │ ├── migrations/
│ │ │ └── versions/
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_auth.py
│ │ └── test_users.py
│ ├── training/ # ML Training Service
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── core/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── database.py
│ │ │ │ └── auth.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── training.py
│ │ │ │ └── models.py
│ │ │ ├── schemas/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── training.py
│ │ │ │ └── models.py
│ │ │ ├── services/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── training_service.py
│ │ │ │ ├── model_service.py
│ │ │ │ └── messaging.py
│ │ │ ├── ml/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── trainer.py
│ │ │ │ ├── prophet_manager.py
│ │ │ │ └── data_processor.py
│ │ │ └── api/
│ │ │ ├── __init__.py
│ │ │ ├── training.py
│ │ │ └── models.py
│ │ ├── migrations/
│ │ │ └── versions/
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_training.py
│ │ └── test_ml.py
│ ├── forecasting/ # Forecasting Service
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── core/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── database.py
│ │ │ │ └── auth.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── forecasts.py
│ │ │ │ └── predictions.py
│ │ │ ├── schemas/
│ │ │ │ ├── __init__.py
│ │ │ │ └── forecasts.py
│ │ │ ├── services/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── forecasting_service.py
│ │ │ │ ├── prediction_service.py
│ │ │ │ └── messaging.py
│ │ │ ├── ml/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── predictor.py
│ │ │ │ └── model_loader.py
│ │ │ └── api/
│ │ │ ├── __init__.py
│ │ │ ├── forecasts.py
│ │ │ └── predictions.py
│ │ ├── migrations/
│ │ │ └── versions/
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_forecasting.py
│ │ └── test_predictions.py
│ ├── data/ # Data Service (External APIs)
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── core/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── database.py
│ │ │ │ └── auth.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── sales.py
│ │ │ │ ├── weather.py
│ │ │ │ └── traffic.py
│ │ │ ├── schemas/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── sales.py
│ │ │ │ └── external.py
│ │ │ ├── services/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── sales_service.py
│ │ │ │ ├── weather_service.py
│ │ │ │ ├── traffic_service.py
│ │ │ │ └── data_import_service.py
│ │ │ ├── external/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aemet.py
│ │ │ │ ├── madrid_opendata.py
│ │ │ │ └── base_client.py
│ │ │ └── api/
│ │ │ ├── __init__.py
│ │ │ ├── sales.py
│ │ │ ├── weather.py
│ │ │ └── traffic.py
│ │ ├── migrations/
│ │ │ └── versions/
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_data.py
│ │ └── test_external.py
│ ├── notification/ # Notification Service
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── core/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── database.py
│ │ │ │ └── auth.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── notifications.py
│ │ │ │ └── templates.py
│ │ │ ├── schemas/
│ │ │ │ ├── __init__.py
│ │ │ │ └── notifications.py
│ │ │ ├── services/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── notification_service.py
│ │ │ │ ├── email_service.py
│ │ │ │ ├── whatsapp_service.py
│ │ │ │ └── messaging.py
│ │ │ └── api/
│ │ │ ├── __init__.py
│ │ │ └── notifications.py
│ │ ├── migrations/
│ │ │ └── versions/
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ └── test_notifications.py
│ └── tenant/ # Tenant Management Service
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── core/
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── database.py
│ │ │ └── auth.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── tenants.py
│ │ │ └── subscriptions.py
│ │ ├── schemas/
│ │ │ ├── __init__.py
│ │ │ └── tenants.py
│ │ ├── services/
│ │ │ ├── __init__.py
│ │ │ ├── tenant_service.py
│ │ │ ├── subscription_service.py
│ │ │ └── messaging.py
│ │ └── api/
│ │ ├── __init__.py
│ │ └── tenants.py
│ ├── migrations/
│ │ └── versions/
│ └── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_tenants.py
├── shared/ # Shared Libraries
│ ├── __init__.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── jwt_handler.py
│ │ └── decorators.py
│ ├── database/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── utils.py
│ ├── messaging/
│ │ ├── __init__.py
│ │ ├── rabbitmq.py
│ │ └── events.py
│ ├── monitoring/
│ │ ├── __init__.py
│ │ ├── logging.py
│ │ └── metrics.py
│ └── utils/
│ ├── __init__.py
│ ├── datetime_utils.py
│ └── validation.py
├── frontend/ # Frontend Applications
│ ├── dashboard/ # React Dashboard
│ │ ├── package.json
│ │ ├── package-lock.json
│ │ ├── Dockerfile
│ │ ├── public/
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ ├── services/
│ │ │ ├── hooks/
│ │ │ └── utils/
│ │ └── tests/
│ └── marketing/ # Next.js Marketing Site
│ ├── package.json
│ ├── package-lock.json
│ ├── Dockerfile
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── styles/
│ │ └── utils/
│ └── tests/
├── infrastructure/ # Infrastructure as Code
│ ├── docker/
│ │ ├── postgres/
│ │ ├── redis/
│ │ └── rabbitmq/
│ ├── kubernetes/
│ │ ├── base/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── production/
│ ├── terraform/
│ │ ├── modules/
│ │ ├── environments/
│ │ └── global/
│ └── monitoring/
│ ├── prometheus/
│ ├── grafana/
│ └── elk/
├── deployment/ # Deployment Configurations
│ ├── docker-compose.dev.yml
│ ├── docker-compose.staging.yml
│ ├── docker-compose.prod.yml
│ ├── nginx/
│ │ ├── nginx.conf
│ │ └── ssl/
│ └── ssl/
└── tests/ # Integration Tests
├── __init__.py
├── integration/
│ ├── __init__.py
│ ├── test_auth_flow.py
│ ├── test_training_flow.py
│ └── test_forecasting_flow.py
├── e2e/
│ ├── __init__.py
│ └── test_complete_flow.py
└── performance/
├── __init__.py
└── test_load.py
```
## 🔧 Key Changes from Monolithic Structure
### 1. **Service Separation**
- Each service has its own database
- Independent deployment and scaling
- Service-specific dependencies
### 2. **API Gateway**
- Single entry point for all clients
- Request routing to appropriate services
- Cross-cutting concerns (auth, logging, rate limiting)
### 3. **Shared Libraries**
- Common functionality across services
- Consistent authentication and database patterns
- Reusable utilities
### 4. **Infrastructure as Code**
- Kubernetes manifests for orchestration
- Terraform for cloud infrastructure
- Docker configurations for all services
### 5. **Independent Testing**
- Each service has its own test suite
- Integration tests for service communication
- End-to-end tests for complete workflows
## 📋 Migration Steps
1. **Create New Project Structure**: Set up all directories and base files
2. **Implement Shared Libraries**: Common functionality first
3. **Build Services One by One**: Start with auth, then data, then training
4. **Set Up API Gateway**: Route requests to services
5. **Configure Infrastructure**: Docker, Kubernetes, monitoring
6. **Migrate Frontend**: Update API calls to use gateway
7. **Set Up CI/CD**: Automated testing and deployment
## 🚀 Benefits of This Structure
- **Independent Development**: Teams can work on different services
- **Independent Deployment**: Deploy services without affecting others
- **Technology Flexibility**: Each service can use different technologies
- **Scalability**: Scale services independently based on load
- **Fault Isolation**: Service failures don't cascade
- **Easier Testing**: Unit tests per service, integration tests across services

View File

@@ -1,18 +0,0 @@
#!/bin/bash
echo "🚀 Deploying Bakery Forecasting Platform..."
# Build and deploy all services
docker-compose build
docker-compose up -d
echo "Waiting for services to be healthy..."
sleep 30
# Check service health
echo "Checking service health..."
curl -f http://localhost:8000/health || echo "Gateway health check failed"
echo "✅ Deployment completed"
echo "Gateway: http://localhost:8000"
echo "API Docs: http://localhost:8000/docs"

View File

@@ -1,14 +0,0 @@
# scripts/docker-logs.sh
#!/bin/bash
# View logs for specific service or all services
SERVICE=${1:-"all"}
if [ "$SERVICE" = "all" ]; then
echo "📋 Showing logs for all services..."
docker-compose logs -f --tail=100
else
echo "📋 Showing logs for $SERVICE..."
docker-compose logs -f --tail=100 $SERVICE
fi

View File

@@ -1,201 +0,0 @@
#!/bin/bash
# scripts/fix-frontend.sh - Frontend troubleshooting script
set -e
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m'
print_step() {
echo -e "${BLUE}[FRONTEND-FIX]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Check if we're in the right directory
if [ ! -f "docker-compose.yml" ]; then
print_error "docker-compose.yml not found. Please run this script from the project root."
exit 1
fi
print_step "Starting frontend troubleshooting..."
# Step 1: Check frontend directory structure
print_step "Checking frontend directory structure..."
if [ ! -d "frontend" ]; then
print_error "Frontend directory not found!"
exit 1
fi
cd frontend
# Step 2: Check package.json exists
if [ ! -f "package.json" ]; then
print_error "package.json not found in frontend directory!"
exit 1
fi
print_success "package.json found"
# Step 3: Generate package-lock.json if missing
if [ ! -f "package-lock.json" ]; then
print_warning "package-lock.json not found. Generating..."
npm install
print_success "package-lock.json generated"
else
print_success "package-lock.json found"
fi
# Step 4: Check for missing directories
print_step "Creating missing directories..."
mkdir -p src/pages
mkdir -p src/components
mkdir -p src/hooks
mkdir -p src/services
mkdir -p src/utils
mkdir -p public
print_success "Directory structure verified"
# Step 5: Check for App.tsx
if [ ! -f "src/App.tsx" ]; then
print_warning "src/App.tsx not found. Creating basic App.tsx..."
cat > src/App.tsx << 'EOF'
import React from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
function App() {
return (
<Router>
<div className="App min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
🥖 PanIA Dashboard
</h1>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">
Bienvenido a PanIA
</h2>
<p className="text-gray-600">
Sistema de predicción de demanda para panaderías en Madrid
</p>
</div>
</div>
</div>
</Router>
)
}
export default App
EOF
print_success "Basic App.tsx created"
else
print_success "App.tsx found"
fi
# Step 6: Check for index.css
if [ ! -f "src/index.css" ]; then
print_warning "src/index.css not found. Creating basic CSS..."
cat > src/index.css << 'EOF'
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
EOF
print_success "Basic index.css created"
else
print_success "index.css found"
fi
# Step 7: Go back to project root
cd ..
# Step 8: Stop and remove the frontend container
print_step "Stopping and removing existing frontend container..."
docker-compose stop dashboard 2>/dev/null || true
docker-compose rm -f dashboard 2>/dev/null || true
# Step 9: Remove the image to force rebuild
print_step "Removing frontend image to force rebuild..."
docker rmi bakery/dashboard:latest 2>/dev/null || true
# Step 10: Check .env file
if [ ! -f ".env" ]; then
print_warning ".env file not found. Creating from example..."
if [ -f ".env.example" ]; then
cp .env.example .env
print_success ".env created from .env.example"
else
# Create minimal .env
cat > .env << 'EOF'
ENVIRONMENT=development
DASHBOARD_PORT=3000
GATEWAY_PORT=8000
IMAGE_TAG=latest
EOF
print_success "Minimal .env created"
fi
fi
# Step 11: Rebuild the frontend service
print_step "Rebuilding frontend service..."
docker-compose build --no-cache dashboard
# Step 12: Start the frontend service
print_step "Starting frontend service..."
docker-compose up -d dashboard
# Step 13: Wait and check status
print_step "Waiting for frontend to start..."
sleep 10
# Check if container is running
if docker-compose ps dashboard | grep -q "Up"; then
print_success "Frontend container is running!"
print_step "Checking logs..."
docker-compose logs --tail=20 dashboard
print_step "Testing frontend health..."
sleep 5
if curl -f http://localhost:3000 >/dev/null 2>&1; then
print_success "Frontend is accessible at http://localhost:3000"
else
print_warning "Frontend is running but not yet accessible. Check logs above."
fi
else
print_error "Frontend container failed to start. Check logs:"
docker-compose logs dashboard
fi
print_step "Frontend troubleshooting completed!"
echo ""
echo "Next steps:"
echo "1. Check the frontend at: http://localhost:3000"
echo "2. View logs with: docker-compose logs dashboard"
echo "3. Restart if needed: docker-compose restart dashboard"

View File

@@ -1,90 +0,0 @@
# scripts/docker-health-check.sh
#!/bin/bash
# Comprehensive health check for all services
services=(
"bakery-redis:6379"
"bakery-rabbitmq:15672"
"bakery-gateway:8000"
"bakery-auth-service:8000"
"bakery-tenant-service:8000"
"bakery-training-service:8000"
"bakery-forecasting-service:8000"
"bakery-data-service:8000"
"bakery-notification-service:8000"
)
echo "🏥 Checking service health..."
for service_port in "${services[@]}"; do
service=$(echo $service_port | cut -d: -f1)
port=$(echo $service_port | cut -d: -f2)
if docker ps --format "table {{.Names}}" | grep -q "^$service$"; then
if [ "$service" = "bakery-redis" ]; then
# Redis health check
if docker exec $service redis-cli -a redis_pass123 ping > /dev/null 2>&1; then
echo "$service is healthy"
else
echo "$service is unhealthy"
fi
elif [ "$service" = "bakery-rabbitmq" ]; then
# RabbitMQ health check
if curl -s -u bakery:forecast123 http://localhost:$port/api/health/checks/alarms > /dev/null; then
echo "$service is healthy"
else
echo "$service is unhealthy"
fi
else
# HTTP service health check
container_ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $service)
if curl -f -s "http://$container_ip:8000/health" > /dev/null; then
echo "$service is healthy"
else
echo "$service is unhealthy"
fi
fi
else
echo "⚠️ $service is not running"
fi
done
echo ""
echo "🔍 Checking database connections..."
databases=("auth-db" "training-db" "forecasting-db" "data-db" "tenant-db" "notification-db")
for db in "${databases[@]}"; do
if docker ps --format "table {{.Names}}" | grep -q "^bakery-$db$"; then
db_name=$(echo $db | sed 's/-/_/g')
user=$(echo $db | sed 's/-db//' | sed 's/-/_/g')_user
if docker exec bakery-$db pg_isready -U $user -d $db_name > /dev/null 2>&1; then
echo "✅ bakery-$db is ready"
else
echo "❌ bakery-$db is not ready"
fi
else
echo "⚠️ bakery-$db is not running"
fi
done
echo ""
echo "📊 Service resource usage:"
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" $(docker ps --format "{{.Names}}" | grep "^bakery-")
# scripts/docker-logs.sh
#!/bin/bash
# View logs for specific service or all services
SERVICE=${1:-"all"}
if [ "$SERVICE" = "all" ]; then
echo "📋 Showing logs for all services..."
docker-compose logs -f --tail=100
else
echo "📋 Showing logs for $SERVICE..."
docker-compose logs -f --tail=100 $SERVICE
fi

View File

@@ -1,106 +0,0 @@
# ================================================================
# SERVICE RESTART SCRIPT
# scripts/restart-service.sh
# ================================================================
#!/bin/bash
# Restart individual service script
set -e
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
print_step() {
echo -e "${BLUE}[RESTART]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
restart_service() {
local service=$1
print_step "Restarting $service..."
# Build the service
docker-compose build $service
# Restart with zero downtime
docker-compose up -d --no-deps --force-recreate $service
# Wait a bit for the service to start
sleep 5
# Check health
local port
case $service in
"gateway") port=8000 ;;
"auth-service") port=8001 ;;
"training-service") port=8002 ;;
"forecasting-service") port=8003 ;;
"data-service") port=8004 ;;
"tenant-service") port=8005 ;;
"notification-service") port=8006 ;;
*)
print_error "Unknown service: $service"
exit 1
;;
esac
# Health check with timeout
local attempts=0
local max_attempts=12 # 60 seconds total
while [ $attempts -lt $max_attempts ]; do
if curl -f -s "http://localhost:$port/health" > /dev/null 2>&1; then
print_success "$service is healthy and ready"
return 0
fi
attempts=$((attempts + 1))
echo "Waiting for $service to be healthy... ($attempts/$max_attempts)"
sleep 5
done
print_error "$service failed to become healthy within 60 seconds"
return 1
}
# Main function
main() {
if [ $# -eq 0 ]; then
echo "Usage: $0 <service-name>"
echo ""
echo "Available services:"
echo " gateway"
echo " auth-service"
echo " training-service"
echo " forecasting-service"
echo " data-service"
echo " tenant-service"
echo " notification-service"
echo ""
echo "Example: $0 auth-service"
exit 1
fi
local service=$1
echo "================================================================"
echo "RESTARTING SERVICE: $service"
echo "================================================================"
restart_service $service
}
# Run main function
main "$@"

View File

@@ -1,392 +0,0 @@
#!/bin/bash
# ================================================================
# Complete Authentication Test with Registration - FIXED VERSION
# Tests the full user lifecycle: registration → login → API access
# ================================================================
echo "🔐 Testing Complete Authentication System with Registration"
echo "=========================================================="
# Configuration
API_BASE="http://localhost:8000"
AUTH_BASE="$API_BASE/api/v1/auth"
TEST_EMAIL="test-$(date +%s)@bakery.com" # Unique email for each test
TEST_PASSWORD="SecurePass123!"
TEST_NAME="Test Baker"
# ✅ FIX: Generate a proper UUID for tenant testing (will be replaced after bakery creation)
TENANT_ID=$(uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || echo "00000000-0000-0000-0000-000000000000")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper function for colored output
log_step() {
echo -e "${BLUE}📍 $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
# Helper function to check if service is healthy
check_service_health() {
local service_url=$1
local service_name=$2
log_step "Checking $service_name health..."
response=$(curl -s -o /dev/null -w "%{http_code}" "$service_url/health")
if [ "$response" = "200" ]; then
log_success "$service_name is healthy"
return 0
else
log_error "$service_name is not healthy (HTTP $response)"
return 1
fi
}
# Check all services are running
log_step "Pre-flight checks..."
echo ""
# Check API Gateway
if ! check_service_health "$API_BASE" "API Gateway"; then
log_error "API Gateway is not running. Start with: docker-compose up -d"
exit 1
fi
# Check Auth Service directly
if ! check_service_health "http://localhost:8001" "Auth Service"; then
log_error "Auth Service is not running. Check: docker-compose logs auth-service"
exit 1
fi
# Check Tenant Service
if ! check_service_health "http://localhost:8005" "Tenant Service"; then
log_error "Tenant Service is not running. Check: docker-compose logs tenant-service"
exit 1
fi
# Check Data Service
if ! check_service_health "http://localhost:8004" "Data Service"; then
log_warning "Data Service is not running, but continuing with auth tests..."
fi
# Check Training Service
if ! check_service_health "http://localhost:8002" "Training Service"; then
log_warning "Training Service is not running, but continuing with auth tests..."
fi
echo ""
log_step "All systems ready! Starting authentication tests..."
echo ""
# ================================================================
# STEP 1: USER REGISTRATION
# ================================================================
log_step "Step 1: Registering new user"
echo "Email: $TEST_EMAIL"
echo "Password: $TEST_PASSWORD"
echo ""
REGISTRATION_RESPONSE=$(curl -s -X POST "$AUTH_BASE/register" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$TEST_EMAIL\",
\"password\": \"$TEST_PASSWORD\",
\"full_name\": \"$TEST_NAME\"
}")
echo "Registration Response:"
echo "$REGISTRATION_RESPONSE" | jq '.'
# Check if registration was successful
if echo "$REGISTRATION_RESPONSE" | jq -e '.id' > /dev/null; then
USER_ID=$(echo "$REGISTRATION_RESPONSE" | jq -r '.id')
log_success "User registration successful! User ID: $USER_ID"
else
log_error "User registration failed!"
echo "Response: $REGISTRATION_RESPONSE"
exit 1
fi
echo ""
# ================================================================
# STEP 2: USER LOGIN
# ================================================================
log_step "Step 2: Logging in with new user credentials"
LOGIN_RESPONSE=$(curl -s -X POST "$AUTH_BASE/login" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$TEST_EMAIL\",
\"password\": \"$TEST_PASSWORD\"
}")
echo "Login Response:"
echo "$LOGIN_RESPONSE" | jq '.'
# Extract access token
if echo "$LOGIN_RESPONSE" | jq -e '.access_token' > /dev/null; then
ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.refresh_token')
log_success "Login successful! Token obtained: ${ACCESS_TOKEN:0:20}..."
else
log_error "Login failed!"
echo "Response: $LOGIN_RESPONSE"
exit 1
fi
echo ""
# ================================================================
# STEP 3: ACCESSING PROTECTED ENDPOINTS
# ================================================================
log_step "Step 3: Testing protected endpoints with authentication"
# 3a. Get current user info
log_step "3a. Getting current user profile"
USER_PROFILE_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/users/me" \
-H "Authorization: Bearer $ACCESS_TOKEN")
echo "User Profile Response:"
echo "$USER_PROFILE_RESPONSE" | jq '.'
if echo "$USER_PROFILE_RESPONSE" | jq -e '.email' > /dev/null; then
log_success "User profile retrieved successfully!"
else
log_warning "User profile endpoint may not be implemented yet"
fi
echo ""
# ================================================================
# STEP 4: TENANT REGISTRATION (BAKERY CREATION)
# ================================================================
log_step "Step 4: Registering a bakery/tenant"
BAKERY_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/register" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Test Bakery $(date +%H%M)\",
\"business_type\": \"bakery\",
\"address\": \"Calle Test 123\",
\"city\": \"Madrid\",
\"postal_code\": \"28001\",
\"phone\": \"+34600123456\"
}")
echo "Bakery Registration Response:"
echo "$BAKERY_RESPONSE" | jq '.'
if echo "$BAKERY_RESPONSE" | jq -e '.id' > /dev/null; then
# ✅ FIX: Use the actual tenant ID returned from bakery creation
TENANT_ID=$(echo "$BAKERY_RESPONSE" | jq -r '.id')
log_success "Bakery registration successful! Tenant ID: $TENANT_ID"
else
log_error "Bakery registration failed!"
echo "Response: $BAKERY_RESPONSE"
# Continue with tests using placeholder UUID for other endpoints
fi
echo ""
# ================================================================
# STEP 5: TEST DATA SERVICE WITH TENANT ID
# ================================================================
log_step "Step 5: Testing data service through gateway"
# Only test with valid tenant ID
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
DATA_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/data/sales" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID")
echo "Data Service Response:"
echo "$DATA_RESPONSE" | jq '.'
if [ "$(echo "$DATA_RESPONSE" | jq -r '.status // "unknown"')" != "error" ]; then
log_success "Data service access successful!"
else
log_warning "Data service returned error (may be expected for new tenant)"
fi
else
log_warning "Skipping data service test - no valid tenant ID"
fi
echo ""
# ================================================================
# STEP 6: TEST TRAINING SERVICE WITH TENANT ID
# ================================================================
log_step "Step 6: Testing training service through gateway"
# Only test with valid tenant ID
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
TRAINING_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/training/jobs" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID" \
-H "Content-Type: application/json" \
-d '{
"include_weather": true,
"include_traffic": false,
"min_data_points": 30
}')
echo "Training Service Response:"
echo "$TRAINING_RESPONSE" | jq '.'
if echo "$TRAINING_RESPONSE" | jq -e '.job_id // .message' > /dev/null; then
log_success "Training service access successful!"
else
log_warning "Training service access may have issues"
fi
else
log_warning "Skipping training service test - no valid tenant ID"
fi
echo ""
# ================================================================
# STEP 7: TOKEN REFRESH
# ================================================================
log_step "Step 7: Testing token refresh"
REFRESH_RESPONSE=$(curl -s -X POST "$AUTH_BASE/refresh" \
-H "Content-Type: application/json" \
-d "{
\"refresh_token\": \"$REFRESH_TOKEN\"
}")
echo "Token Refresh Response:"
echo "$REFRESH_RESPONSE" | jq '.'
if echo "$REFRESH_RESPONSE" | jq -e '.access_token' > /dev/null; then
NEW_ACCESS_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.access_token')
log_success "Token refresh successful! New token: ${NEW_ACCESS_TOKEN:0:20}..."
else
log_warning "Token refresh may not be fully implemented"
fi
echo ""
# ================================================================
# STEP 8: DIRECT SERVICE HEALTH CHECKS
# ================================================================
log_step "Step 8: Testing direct service access (without gateway)"
# Test auth service directly
log_step "8a. Auth service direct health check"
AUTH_HEALTH=$(curl -s -X GET "http://localhost:8001/health")
echo "Auth Service Health:"
echo "$AUTH_HEALTH" | jq '.'
# Test other services if available
log_step "8b. Other services health check"
services=("8002:Training" "8003:Forecasting" "8004:Data" "8005:Tenant" "8006:Notification")
for service in "${services[@]}"; do
port=$(echo $service | cut -d: -f1)
name=$(echo $service | cut -d: -f2)
health_response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/health" 2>/dev/null)
if [ "$health_response" = "200" ]; then
log_success "$name Service (port $port) is healthy"
else
log_warning "$name Service (port $port) is not responding"
fi
done
echo ""
# ================================================================
# STEP 9: LOGOUT
# ================================================================
log_step "Step 9: Logging out user"
LOGOUT_RESPONSE=$(curl -s -X POST "$AUTH_BASE/logout" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json")
echo "Logout Response:"
echo "$LOGOUT_RESPONSE" | jq '.'
if echo "$LOGOUT_RESPONSE" | jq -e '.message' > /dev/null; then
log_success "Logout successful!"
else
log_warning "Logout endpoint may not be fully implemented"
fi
echo ""
# ================================================================
# SUMMARY
# ================================================================
echo "🎉 Authentication Test Summary"
echo "==============================="
echo ""
echo "Test User Created:"
echo " 📧 Email: $TEST_EMAIL"
echo " 👤 Name: $TEST_NAME"
echo " 🆔 User ID: $USER_ID"
echo ""
echo "Authentication Flow:"
echo " ✅ User Registration"
echo " ✅ User Login"
echo " ✅ Token Verification"
echo " ✅ Protected Endpoint Access"
echo " ✅ Token Refresh"
echo " ✅ User Logout"
echo ""
echo "Services Tested:"
echo " 🌐 API Gateway"
echo " 🔐 Auth Service"
echo " 🏢 Tenant Service (bakery registration)"
echo " 📊 Data Service (through gateway)"
echo " 🤖 Training Service (through gateway)"
echo ""
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
echo "Tenant Created:"
echo " 🏪 Tenant ID: $TENANT_ID"
echo ""
fi
log_success "Complete authentication test finished successfully!"
echo ""
echo "🔧 Development Tips:"
echo " • Use the created test user for further development"
echo " • Check service logs with: docker-compose logs [service-name]"
echo " • View API docs at: http://localhost:8000/docs"
echo " • Monitor services at: http://localhost:3002"
echo ""
echo "🧹 Cleanup:"
echo " • Test user will remain in database for development"
echo " • To reset: Delete user from auth database or run cleanup script"

View File

@@ -49,7 +49,9 @@ async def get_current_user_info(
)
# ✅ FIX: Fetch full user from database to get the real role
user = await UserService.get_user_by_id(user_id, db)
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
user = await user_repo.get_by_id(user_id)
logger.debug(f"Fetched user from DB - Role: {user.role}, Email: {user.email}")
@@ -104,7 +106,21 @@ async def update_current_user(
"""Update current user information"""
try:
user_id = current_user.get("user_id") if isinstance(current_user, dict) else current_user.id
updated_user = await UserService.update_user(user_id, user_update, db)
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
# Prepare update data
update_data = {}
if user_update.full_name is not None:
update_data["full_name"] = user_update.full_name
if user_update.phone is not None:
update_data["phone"] = user_update.phone
if user_update.language is not None:
update_data["language"] = user_update.language
if user_update.timezone is not None:
update_data["timezone"] = user_update.timezone
updated_user = await user_repo.update(user_id, update_data)
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,

View File

@@ -259,7 +259,7 @@ class EnhancedAuthService:
"""Logout user using repository pattern"""
try:
async with self.database_manager.get_session() as session:
token_repo = TokenRepository(session)
token_repo = TokenRepository(RefreshToken, session)
# Revoke specific refresh token using repository
success = await token_repo.revoke_token(user_id, refresh_token)
@@ -291,8 +291,8 @@ class EnhancedAuthService:
)
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
user_repo = UserRepository(User, session)
token_repo = TokenRepository(RefreshToken, session)
# Validate refresh token using repository
is_valid = await token_repo.validate_refresh_token(refresh_token, user_id)
@@ -364,7 +364,7 @@ class EnhancedAuthService:
"""Get user profile using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
user = await user_repo.get_by_id(user_id)
if not user:
@@ -394,7 +394,7 @@ class EnhancedAuthService:
"""Update user profile using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
updated_user = await user_repo.update(user_id, update_data)
if not updated_user:
@@ -429,8 +429,8 @@ class EnhancedAuthService:
"""Change user password using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
user_repo = UserRepository(User, session)
token_repo = TokenRepository(RefreshToken, session)
# Get user and verify old password
user = await user_repo.get_by_id(user_id)
@@ -470,7 +470,7 @@ class EnhancedAuthService:
"""Verify user email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
# In a real implementation, you'd verify the verification_token
# For now, just mark user as verified
@@ -493,8 +493,8 @@ class EnhancedAuthService:
"""Deactivate user account using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
user_repo = UserRepository(User, session)
token_repo = TokenRepository(RefreshToken, session)
# Update user status
updated_user = await user_repo.update(user_id, {"is_active": False})

View File

@@ -10,6 +10,7 @@ import structlog
from app.repositories import UserRepository, TokenRepository
from app.schemas.auth import UserResponse, UserUpdate
from app.models.users import User, RefreshToken
from app.core.security import SecurityManager
from shared.database.unit_of_work import UnitOfWork
from shared.database.transactions import transactional
@@ -29,7 +30,7 @@ class EnhancedUserService:
"""Get user by ID using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
user = await user_repo.get_by_id(user_id)
if not user:
@@ -58,7 +59,7 @@ class EnhancedUserService:
"""Get user by email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
user = await user_repo.get_by_email(email)
if not user:
@@ -93,7 +94,7 @@ class EnhancedUserService:
"""Get paginated list of users using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
filters = {}
if active_only:
@@ -139,7 +140,7 @@ class EnhancedUserService:
"""Update user information using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(db_session)
user_repo = UserRepository(User, db_session)
# Validate user exists
existing_user = await user_repo.get_by_id(user_id)
@@ -298,7 +299,7 @@ class EnhancedUserService:
"""Activate user account using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(db_session)
user_repo = UserRepository(User, db_session)
# Update user status
updated_user = await user_repo.update(user_id, {"is_active": True})
@@ -321,7 +322,7 @@ class EnhancedUserService:
"""Verify user email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
# In a real implementation, you'd verify the verification_token
# For now, just mark user as verified
@@ -344,7 +345,7 @@ class EnhancedUserService:
"""Get user statistics using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
# Get basic user statistics
statistics = await user_repo.get_user_statistics()
@@ -372,7 +373,7 @@ class EnhancedUserService:
"""Search users by email or name using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
users = await user_repo.search_users(
search_term, role, active_only, skip, limit
@@ -409,7 +410,7 @@ class EnhancedUserService:
"""Update user role using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user_repo = UserRepository(User, session)
# Validate role
valid_roles = ["user", "admin", "super_admin"]
@@ -452,8 +453,8 @@ class EnhancedUserService:
"""Get user activity information using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
user_repo = UserRepository(User, session)
token_repo = TokenRepository(RefreshToken, session)
# Get user
user = await user_repo.get_by_id(user_id)

View File

@@ -197,7 +197,7 @@ class ForecastingBaseRepository(BaseRepository):
errors = []
for field in required_fields:
if field not in data or not data[field]:
if field not in data or data[field] is None:
errors.append(f"Missing required field: {field}")
# Validate tenant_id format if present

View File

@@ -1,219 +0,0 @@
#!/usr/bin/env python3
"""
Comprehensive test to verify all services can start correctly after database refactoring
"""
import subprocess
import time
import sys
from typing import Dict, List, Tuple
# Service configurations
SERVICES = {
"infrastructure": {
"services": ["redis", "rabbitmq", "auth-db", "data-db", "tenant-db",
"forecasting-db", "notification-db", "training-db", "prometheus"],
"wait_time": 60
},
"core": {
"services": ["auth-service", "data-service"],
"wait_time": 45,
"health_checks": [
("auth-service", "http://localhost:8001/health"),
("data-service", "http://localhost:8002/health"),
]
},
"business": {
"services": ["tenant-service", "training-service", "forecasting-service",
"notification-service"],
"wait_time": 45,
"health_checks": [
("tenant-service", "http://localhost:8003/health"),
("training-service", "http://localhost:8004/health"),
("forecasting-service", "http://localhost:8005/health"),
("notification-service", "http://localhost:8006/health"),
]
},
"ui": {
"services": ["gateway", "dashboard"],
"wait_time": 30,
"health_checks": [
("gateway", "http://localhost:8000/health"),
]
}
}
def run_command(command: str, description: str = None) -> Tuple[int, str]:
"""Run a shell command and return exit code and output"""
if description:
print(f"Running: {command}")
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=120
)
return result.returncode, result.stdout + result.stderr
except subprocess.TimeoutExpired:
return -1, f"Command timed out: {command}"
def check_container_status() -> Dict[str, str]:
"""Get status of all containers"""
exit_code, output = run_command("docker compose ps")
if exit_code != 0:
return {}
status_dict = {}
lines = output.strip().split('\n')[1:] # Skip header
for line in lines:
if line.strip():
parts = line.split()
if len(parts) >= 4:
name = parts[0].replace('bakery-', '')
status = ' '.join(parts[3:])
status_dict[name] = status
return status_dict
def check_health(service: str, url: str) -> bool:
"""Check if a service health endpoint is responding"""
try:
# Use curl for health checks instead of requests
exit_code, _ = run_command(f"curl -f {url}", None)
return exit_code == 0
except Exception as e:
print(f"❌ Health check failed for {service}: {str(e)}")
return False
def wait_for_healthy_containers(services: List[str], max_wait: int = 60) -> bool:
"""Wait for containers to become healthy"""
print(f"⏳ Waiting up to {max_wait} seconds for services to become healthy...")
for i in range(max_wait):
status = check_container_status()
healthy_count = 0
for service in services:
service_status = status.get(service, "not found")
if "healthy" in service_status.lower() or "up" in service_status.lower():
healthy_count += 1
if healthy_count == len(services):
print(f"✅ All {len(services)} services are healthy after {i+1} seconds")
return True
time.sleep(1)
print(f"⚠️ Only {healthy_count}/{len(services)} services became healthy")
return False
def test_service_group(group_name: str, config: Dict) -> bool:
"""Test a group of services"""
print(f"\n🧪 Testing {group_name} services...")
print(f"Services: {', '.join(config['services'])}")
# Start services
services_str = ' '.join(config['services'])
exit_code, output = run_command(f"docker compose up -d {services_str}")
if exit_code != 0:
print(f"❌ Failed to start {group_name} services")
print(output[-1000:]) # Last 1000 chars of output
return False
print(f"{group_name.title()} services started")
# Wait for services to be healthy
if not wait_for_healthy_containers(config['services'], config['wait_time']):
print(f"⚠️ Some {group_name} services didn't become healthy")
# Show container status
status = check_container_status()
for service in config['services']:
service_status = status.get(service, "not found")
print(f" {service}: {service_status}")
# Show logs for failed services
for service in config['services']:
service_status = status.get(service, "")
if "unhealthy" in service_status.lower() or "restarting" in service_status.lower():
print(f"\n📋 Logs for failed service {service}:")
_, logs = run_command(f"docker compose logs --tail=10 {service}")
print(logs[-800:]) # Last 800 chars
return False
# Run health checks if defined
if "health_checks" in config:
print(f"🔍 Running health checks for {group_name} services...")
for service, url in config['health_checks']:
if check_health(service, url):
print(f"{service} health check passed")
else:
print(f"{service} health check failed")
return False
print(f"🎉 {group_name.title()} services test PASSED!")
return True
def main():
"""Main test function"""
print("🔧 COMPREHENSIVE SERVICES STARTUP TEST")
print("=" * 50)
# Clean up any existing containers
print("🧹 Cleaning up existing containers...")
run_command("docker compose down")
all_passed = True
# Test each service group in order
for group_name, config in SERVICES.items():
if not test_service_group(group_name, config):
all_passed = False
break
# Final status check
print("\n📊 Final container status:")
status = check_container_status()
healthy_count = 0
total_count = 0
for group_config in SERVICES.values():
for service in group_config['services']:
total_count += 1
service_status = status.get(service, "not found")
status_icon = "" if ("healthy" in service_status.lower() or "up" in service_status.lower()) else ""
if "healthy" in service_status.lower() or "up" in service_status.lower():
healthy_count += 1
print(f"{status_icon} {service}: {service_status}")
# Clean up
print("\n🧹 Cleaning up containers...")
run_command("docker compose down")
# Final result
print("=" * 50)
if all_passed and healthy_count == total_count:
print(f"🎉 ALL SERVICES TEST PASSED!")
print(f"{healthy_count}/{total_count} services started successfully")
print("💡 Your docker-compose setup is working correctly")
print("🚀 You can now run: docker compose up -d")
return 0
else:
print(f"❌ SERVICES TEST FAILED")
print(f"⚠️ {healthy_count}/{total_count} services started successfully")
print("💡 Check the logs above for details")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,295 +0,0 @@
#!/usr/bin/env python3
"""
Docker Build and Compose Test Script
Tests that each service can be built correctly and docker-compose starts without errors
"""
import os
import sys
import subprocess
import time
import json
from pathlib import Path
def run_command(cmd, cwd=None, timeout=300, capture_output=True):
"""Run a shell command with timeout and error handling"""
try:
print(f"Running: {cmd}")
if capture_output:
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
timeout=timeout,
capture_output=True,
text=True
)
else:
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
timeout=timeout
)
return result
except subprocess.TimeoutExpired:
print(f"Command timed out after {timeout} seconds: {cmd}")
return None
except Exception as e:
print(f"Error running command: {e}")
return None
def check_docker_available():
"""Check if Docker is available and running"""
print("🐳 Checking Docker availability...")
# Check if docker command exists
result = run_command("which docker")
if result.returncode != 0:
print("❌ Docker command not found. Please install Docker.")
return False
# Check if Docker daemon is running
result = run_command("docker version")
if result.returncode != 0:
print("❌ Docker daemon is not running. Please start Docker.")
return False
# Check if docker-compose is available
result = run_command("docker compose version")
if result.returncode != 0:
# Try legacy docker-compose
result = run_command("docker-compose version")
if result.returncode != 0:
print("❌ docker-compose not found. Please install docker-compose.")
return False
else:
print("✅ Using legacy docker-compose")
else:
print("✅ Using Docker Compose v2")
print("✅ Docker is available and running")
return True
def test_individual_builds():
"""Test building each service individually"""
print("\n🔨 Testing individual service builds...")
services = [
"auth-service",
"tenant-service",
"training-service",
"forecasting-service",
"data-service",
"notification-service"
]
build_results = {}
for service in services:
print(f"\n--- Building {service} ---")
dockerfile_path = f"./services/{service.replace('-service', '')}/Dockerfile"
if not os.path.exists(dockerfile_path):
print(f"❌ Dockerfile not found: {dockerfile_path}")
build_results[service] = False
continue
# Build the service
cmd = f"docker build -t bakery/{service}:test -f {dockerfile_path} ."
result = run_command(cmd, timeout=600)
if result and result.returncode == 0:
print(f"{service} built successfully")
build_results[service] = True
else:
print(f"{service} build failed")
if result and result.stderr:
print(f"Error: {result.stderr}")
build_results[service] = False
return build_results
def test_docker_compose_config():
"""Test docker-compose configuration"""
print("\n📋 Testing docker-compose configuration...")
# Check if docker-compose.yml exists
if not os.path.exists("docker-compose.yml"):
print("❌ docker-compose.yml not found")
return False
# Check if .env file exists
if not os.path.exists(".env"):
print("❌ .env file not found")
return False
# Validate docker-compose configuration
result = run_command("docker compose config")
if result.returncode != 0:
# Try legacy docker-compose
result = run_command("docker-compose config")
if result.returncode != 0:
print("❌ docker-compose configuration validation failed")
if result.stderr:
print(f"Error: {result.stderr}")
return False
print("✅ docker-compose configuration is valid")
return True
def test_essential_services_startup():
"""Test starting essential infrastructure services only"""
print("\n🚀 Testing essential services startup...")
# Start only databases and infrastructure - not the application services
essential_services = [
"redis",
"rabbitmq",
"auth-db",
"data-db"
]
try:
# Stop any running containers first
print("Stopping any existing containers...")
run_command("docker compose down", timeout=120)
# Start essential services
services_str = " ".join(essential_services)
cmd = f"docker compose up -d {services_str}"
result = run_command(cmd, timeout=300)
if result.returncode != 0:
print("❌ Failed to start essential services")
if result.stderr:
print(f"Error: {result.stderr}")
return False
# Wait for services to be ready
print("Waiting for services to be ready...")
time.sleep(30)
# Check service health
print("Checking service health...")
result = run_command("docker compose ps")
if result.returncode == 0:
print("Services status:")
print(result.stdout)
print("✅ Essential services started successfully")
return True
except Exception as e:
print(f"❌ Error during essential services test: {e}")
return False
finally:
# Cleanup
print("Cleaning up essential services test...")
run_command("docker compose down", timeout=120)
def cleanup_docker_resources():
"""Clean up Docker resources"""
print("\n🧹 Cleaning up Docker resources...")
# Remove test images
services = ["auth-service", "tenant-service", "training-service",
"forecasting-service", "data-service", "notification-service"]
for service in services:
run_command(f"docker rmi bakery/{service}:test", timeout=30)
# Remove dangling images
run_command("docker image prune -f", timeout=60)
print("✅ Docker resources cleaned up")
def main():
"""Main test function"""
print("🔍 DOCKER BUILD AND COMPOSE TESTING")
print("=" * 50)
base_path = Path(__file__).parent
os.chdir(base_path)
# Test results
test_results = {
"docker_available": False,
"compose_config": False,
"individual_builds": {},
"essential_services": False
}
# Check Docker availability
test_results["docker_available"] = check_docker_available()
if not test_results["docker_available"]:
print("\n❌ Docker tests cannot proceed without Docker")
return 1
# Test docker-compose configuration
test_results["compose_config"] = test_docker_compose_config()
# Test individual builds (this might take a while)
print("\n⚠️ Individual builds test can take several minutes...")
user_input = input("Run individual service builds? (y/N): ").lower()
if user_input == 'y':
test_results["individual_builds"] = test_individual_builds()
else:
print("Skipping individual builds test")
test_results["individual_builds"] = {"skipped": True}
# Test essential services startup
print("\n⚠️ Essential services test will start Docker containers...")
user_input = input("Test essential services startup? (y/N): ").lower()
if user_input == 'y':
test_results["essential_services"] = test_essential_services_startup()
else:
print("Skipping essential services test")
test_results["essential_services"] = "skipped"
# Print final results
print("\n" + "=" * 50)
print("📋 TEST RESULTS SUMMARY")
print("=" * 50)
print(f"Docker Available: {'' if test_results['docker_available'] else ''}")
print(f"Docker Compose Config: {'' if test_results['compose_config'] else ''}")
if "skipped" in test_results["individual_builds"]:
print("Individual Builds: ⏭️ Skipped")
else:
builds = test_results["individual_builds"]
success_count = sum(1 for v in builds.values() if v)
total_count = len(builds)
print(f"Individual Builds: {success_count}/{total_count}")
for service, success in builds.items():
status = "" if success else ""
print(f" - {service}: {status}")
if test_results["essential_services"] == "skipped":
print("Essential Services: ⏭️ Skipped")
else:
print(f"Essential Services: {'' if test_results['essential_services'] else ''}")
# Cleanup
if user_input == 'y' and "skipped" not in test_results["individual_builds"]:
cleanup_docker_resources()
# Determine overall success
if (test_results["docker_available"] and
test_results["compose_config"] and
(test_results["individual_builds"] == {"skipped": True} or
all(test_results["individual_builds"].values())) and
(test_results["essential_services"] == "skipped" or test_results["essential_services"])):
print("\n🎉 ALL TESTS PASSED")
return 0
else:
print("\n❌ SOME TESTS FAILED")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)

View File

@@ -1,300 +0,0 @@
#!/usr/bin/env python3
"""
Automated Docker Build and Compose Test Script
Tests that each service can be built correctly and docker-compose starts without errors
"""
import os
import sys
import subprocess
import time
import json
from pathlib import Path
def run_command(cmd, cwd=None, timeout=300, capture_output=True):
"""Run a shell command with timeout and error handling"""
try:
print(f"Running: {cmd}")
if capture_output:
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
timeout=timeout,
capture_output=True,
text=True
)
else:
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
timeout=timeout
)
return result
except subprocess.TimeoutExpired:
print(f"Command timed out after {timeout} seconds: {cmd}")
return None
except Exception as e:
print(f"Error running command: {e}")
return None
def check_docker_available():
"""Check if Docker is available and running"""
print("🐳 Checking Docker availability...")
# Check if docker command exists
result = run_command("which docker")
if result.returncode != 0:
print("❌ Docker command not found. Please install Docker.")
return False
# Check if Docker daemon is running
result = run_command("docker version")
if result.returncode != 0:
print("❌ Docker daemon is not running. Please start Docker.")
return False
# Check if docker-compose is available
result = run_command("docker compose version")
if result.returncode != 0:
# Try legacy docker-compose
result = run_command("docker-compose version")
if result.returncode != 0:
print("❌ docker-compose not found. Please install docker-compose.")
return False
else:
print("✅ Using legacy docker-compose")
else:
print("✅ Using Docker Compose v2")
print("✅ Docker is available and running")
return True
def test_docker_compose_config():
"""Test docker-compose configuration"""
print("\n📋 Testing docker-compose configuration...")
# Check if docker-compose.yml exists
if not os.path.exists("docker-compose.yml"):
print("❌ docker-compose.yml not found")
return False
# Check if .env file exists
if not os.path.exists(".env"):
print("❌ .env file not found")
return False
# Validate docker-compose configuration
result = run_command("docker compose config")
if result.returncode != 0:
# Try legacy docker-compose
result = run_command("docker-compose config")
if result.returncode != 0:
print("❌ docker-compose configuration validation failed")
if result.stderr:
print(f"Error: {result.stderr}")
return False
print("✅ docker-compose configuration is valid")
return True
def test_dockerfile_syntax():
"""Test that each Dockerfile has valid syntax"""
print("\n📄 Testing Dockerfile syntax...")
services = [
("auth", "services/auth/Dockerfile"),
("tenant", "services/tenant/Dockerfile"),
("training", "services/training/Dockerfile"),
("forecasting", "services/forecasting/Dockerfile"),
("data", "services/data/Dockerfile"),
("notification", "services/notification/Dockerfile")
]
dockerfile_results = {}
for service_name, dockerfile_path in services:
print(f"\n--- Checking {service_name} Dockerfile ---")
if not os.path.exists(dockerfile_path):
print(f"❌ Dockerfile not found: {dockerfile_path}")
dockerfile_results[service_name] = False
continue
# Check Dockerfile syntax using docker build --dry-run (if available)
# Otherwise just check if file exists and is readable
try:
with open(dockerfile_path, 'r') as f:
content = f.read()
if 'FROM' not in content:
print(f"{service_name} Dockerfile missing FROM instruction")
dockerfile_results[service_name] = False
elif 'WORKDIR' not in content:
print(f"⚠️ {service_name} Dockerfile missing WORKDIR instruction")
dockerfile_results[service_name] = True
else:
print(f"{service_name} Dockerfile syntax looks good")
dockerfile_results[service_name] = True
except Exception as e:
print(f"❌ Error reading {service_name} Dockerfile: {e}")
dockerfile_results[service_name] = False
return dockerfile_results
def check_requirements_files():
"""Check that requirements.txt files exist for each service"""
print("\n📦 Checking requirements.txt files...")
services = ["auth", "tenant", "training", "forecasting", "data", "notification"]
requirements_results = {}
for service in services:
req_path = f"services/{service}/requirements.txt"
if os.path.exists(req_path):
print(f"{service} requirements.txt found")
requirements_results[service] = True
else:
print(f"{service} requirements.txt missing")
requirements_results[service] = False
return requirements_results
def test_service_main_files():
"""Check that each service has a main.py file"""
print("\n🐍 Checking service main.py files...")
services = ["auth", "tenant", "training", "forecasting", "data", "notification"]
main_file_results = {}
for service in services:
main_path = f"services/{service}/app/main.py"
if os.path.exists(main_path):
try:
with open(main_path, 'r') as f:
content = f.read()
if 'FastAPI' in content or 'app = ' in content:
print(f"{service} main.py looks good")
main_file_results[service] = True
else:
print(f"⚠️ {service} main.py exists but may not be a FastAPI app")
main_file_results[service] = True
except Exception as e:
print(f"❌ Error reading {service} main.py: {e}")
main_file_results[service] = False
else:
print(f"{service} main.py missing")
main_file_results[service] = False
return main_file_results
def quick_build_test():
"""Quick test to see if one service can build (faster than full build)"""
print("\n⚡ Quick build test (data service only)...")
# Test building just the data service
cmd = "docker build --no-cache -t bakery/data-service:quick-test -f services/data/Dockerfile ."
result = run_command(cmd, timeout=600)
if result and result.returncode == 0:
print("✅ Data service quick build successful")
# Cleanup
run_command("docker rmi bakery/data-service:quick-test", timeout=30)
return True
else:
print("❌ Data service quick build failed")
if result and result.stderr:
print(f"Build error: {result.stderr[-1000:]}") # Last 1000 chars
return False
def main():
"""Main test function"""
print("🔍 AUTOMATED DOCKER BUILD AND COMPOSE TESTING")
print("=" * 60)
base_path = Path(__file__).parent
os.chdir(base_path)
# Test results
test_results = {
"docker_available": False,
"compose_config": False,
"dockerfile_syntax": {},
"requirements_files": {},
"main_files": {},
"quick_build": False
}
# Check Docker availability
test_results["docker_available"] = check_docker_available()
if not test_results["docker_available"]:
print("\n❌ Docker tests cannot proceed without Docker")
return 1
# Test docker-compose configuration
test_results["compose_config"] = test_docker_compose_config()
# Test Dockerfile syntax
test_results["dockerfile_syntax"] = test_dockerfile_syntax()
# Check requirements files
test_results["requirements_files"] = check_requirements_files()
# Check main.py files
test_results["main_files"] = test_service_main_files()
# Quick build test
test_results["quick_build"] = quick_build_test()
# Print final results
print("\n" + "=" * 60)
print("📋 TEST RESULTS SUMMARY")
print("=" * 60)
print(f"Docker Available: {'' if test_results['docker_available'] else ''}")
print(f"Docker Compose Config: {'' if test_results['compose_config'] else ''}")
# Dockerfile syntax results
dockerfile_success = all(test_results["dockerfile_syntax"].values())
print(f"Dockerfile Syntax: {'' if dockerfile_success else ''}")
for service, success in test_results["dockerfile_syntax"].items():
status = "" if success else ""
print(f" - {service}: {status}")
# Requirements files results
req_success = all(test_results["requirements_files"].values())
print(f"Requirements Files: {'' if req_success else ''}")
for service, success in test_results["requirements_files"].items():
status = "" if success else ""
print(f" - {service}: {status}")
# Main files results
main_success = all(test_results["main_files"].values())
print(f"Main.py Files: {'' if main_success else ''}")
for service, success in test_results["main_files"].items():
status = "" if success else ""
print(f" - {service}: {status}")
print(f"Quick Build Test: {'' if test_results['quick_build'] else ''}")
# Determine overall success
overall_success = (
test_results["docker_available"] and
test_results["compose_config"] and
dockerfile_success and
req_success and
main_success and
test_results["quick_build"]
)
if overall_success:
print("\n🎉 ALL TESTS PASSED - Services should build and start correctly!")
print("💡 You can now run: docker compose up -d")
return 0
else:
print("\n❌ SOME TESTS FAILED - Please fix issues before running docker compose")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)

View File

@@ -1,151 +0,0 @@
#!/usr/bin/env python3
"""
Simple Docker Services Test
Tests that docker-compose can start services without external dependencies
"""
import os
import sys
import subprocess
import time
from pathlib import Path
def run_command(cmd, timeout=300):
"""Run a shell command with timeout"""
try:
print(f"Running: {cmd}")
result = subprocess.run(
cmd,
shell=True,
timeout=timeout,
capture_output=True,
text=True
)
return result
except subprocess.TimeoutExpired:
print(f"Command timed out after {timeout} seconds")
return None
except Exception as e:
print(f"Error running command: {e}")
return None
def test_infrastructure_services():
"""Test starting just infrastructure services"""
print("🏗️ Testing infrastructure services...")
try:
# Stop any existing containers
print("Cleaning up existing containers...")
run_command("docker compose down", timeout=120)
# Start only infrastructure services
infra_services = "redis rabbitmq auth-db data-db"
cmd = f"docker compose up -d {infra_services}"
result = run_command(cmd, timeout=300)
if result and result.returncode == 0:
print("✅ Infrastructure services started")
# Wait a bit for services to initialize
print("Waiting 30 seconds for services to initialize...")
time.sleep(30)
# Check container status
status_result = run_command("docker compose ps", timeout=30)
if status_result and status_result.stdout:
print("Container status:")
print(status_result.stdout)
# Try to start one application service
print("Testing application service startup...")
app_result = run_command("docker compose up -d auth-service", timeout=180)
if app_result and app_result.returncode == 0:
print("✅ Auth service started successfully")
# Wait for it to initialize
time.sleep(20)
# Check health with curl
health_result = run_command("curl -f http://localhost:8001/health", timeout=10)
if health_result and health_result.returncode == 0:
print("✅ Auth service is healthy!")
return True
else:
print("⚠️ Auth service started but health check failed")
# Show logs for debugging
logs_result = run_command("docker compose logs --tail=20 auth-service", timeout=30)
if logs_result and logs_result.stdout:
print("Auth service logs:")
print(logs_result.stdout)
return False
else:
print("❌ Failed to start auth service")
if app_result and app_result.stderr:
print(f"Error: {app_result.stderr}")
return False
else:
print("❌ Failed to start infrastructure services")
if result and result.stderr:
print(f"Error: {result.stderr}")
return False
except Exception as e:
print(f"❌ Error during infrastructure test: {e}")
return False
def show_final_status():
"""Show final container status"""
print("\n📊 Final container status:")
result = run_command("docker compose ps", timeout=30)
if result and result.stdout:
print(result.stdout)
def cleanup():
"""Clean up containers"""
print("\n🧹 Cleaning up containers...")
run_command("docker compose down", timeout=180)
print("✅ Cleanup completed")
def main():
"""Main test function"""
print("🔧 SIMPLE DOCKER SERVICES TEST")
print("=" * 40)
base_path = Path(__file__).parent
os.chdir(base_path)
success = False
try:
# Test infrastructure services
success = test_infrastructure_services()
# Show current status
show_final_status()
except KeyboardInterrupt:
print("\n⚠️ Test interrupted by user")
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
finally:
# Always cleanup
cleanup()
# Final result
print("\n" + "=" * 40)
if success:
print("🎉 DOCKER SERVICES TEST PASSED!")
print("✅ Services can start and respond to health checks")
print("💡 Your docker-compose setup is working correctly")
print("🚀 You can now run: docker compose up -d")
return 0
else:
print("❌ DOCKER SERVICES TEST FAILED")
print("⚠️ Some issues were found with service startup")
print("💡 Check the logs above for details")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)

View File

@@ -1,78 +0,0 @@
#\!/bin/bash
echo "🧪 Testing Forecasting Service Components - FIXED ROUTES"
echo "======================================================"
# Use the most recent successful tenant from the full onboarding test
TENANT_ID="5765e61a-4d06-4e17-b614-a4f8410e2a35"
# Get a fresh access token
echo "1. Getting access token..."
LOGIN_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "onboarding.test.1754565461@bakery.com", "password": "TestPassword123\!"}')
ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('access_token', ''))
except:
pass
" 2>/dev/null)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Login failed"
echo "Response: $LOGIN_RESPONSE"
exit 1
fi
echo "✅ Access token obtained"
# Test 1: Test forecast endpoint with correct path format (no extra path)
echo ""
echo "2. Testing forecast endpoint with correct format..."
FORECAST_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/forecasts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_name": "Cafe", "days_ahead": 7}')
HTTP_CODE=$(echo "$FORECAST_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
FORECAST_RESPONSE=$(echo "$FORECAST_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Forecast HTTP Code: $HTTP_CODE"
echo "Forecast Response:"
echo "$FORECAST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$FORECAST_RESPONSE"
# Test 2: Test predictions endpoint with correct format
echo ""
echo "3. Testing predictions endpoint with correct format..."
PREDICTION_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/predictions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_names": ["Cafe", "Pan"], "days_ahead": 5, "include_confidence": true}')
PRED_HTTP_CODE=$(echo "$PREDICTION_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
PREDICTION_RESPONSE=$(echo "$PREDICTION_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Prediction HTTP Code: $PRED_HTTP_CODE"
echo "Prediction Response:"
echo "$PREDICTION_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$PREDICTION_RESPONSE"
# Test 3: Direct forecasting service test (bypass gateway)
echo ""
echo "4. Testing forecasting service directly (bypass gateway)..."
DIRECT_FORECAST=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8003/api/v1/tenants/$TENANT_ID/forecasts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_name": "Cafe", "days_ahead": 7}')
DIRECT_HTTP_CODE=$(echo "$DIRECT_FORECAST" | grep "HTTP_CODE:" | cut -d: -f2)
DIRECT_FORECAST=$(echo "$DIRECT_FORECAST" | sed '/HTTP_CODE:/d')
echo "Direct Forecast HTTP Code: $DIRECT_HTTP_CODE"
echo "Direct Forecast Response:"
echo "$DIRECT_FORECAST" | python3 -m json.tool 2>/dev/null || echo "$DIRECT_FORECAST"
echo ""
echo "🏁 Fixed forecasting test completed\!"

View File

@@ -1,83 +0,0 @@
#!/bin/bash
echo "🧪 Testing Forecasting Service Components"
echo "========================================"
# Use the most recent successful tenant from the full onboarding test
TENANT_ID="5765e61a-4d06-4e17-b614-a4f8410e2a35"
# Get a fresh access token
echo "1. Getting access token..."
LOGIN_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "onboarding.test.1754565461@bakery.com", "password": "TestPassword123!"}')
ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('access_token', ''))
except:
pass
" 2>/dev/null)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Login failed"
echo "Response: $LOGIN_RESPONSE"
exit 1
fi
echo "✅ Access token obtained"
# Test 1: Check forecasting service health
echo ""
echo "2. Testing forecasting service health..."
FORECAST_HEALTH=$(curl -s "http://localhost:8003/health")
echo "Forecasting Service Health:"
echo "$FORECAST_HEALTH" | python3 -m json.tool 2>/dev/null || echo "$FORECAST_HEALTH"
# Test 2: Test forecast endpoint (should handle gracefully if no models exist)
echo ""
echo "3. Testing forecast endpoint..."
FORECAST_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/forecasts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_name": "bread", "days_ahead": 7}')
HTTP_CODE=$(echo "$FORECAST_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
FORECAST_RESPONSE=$(echo "$FORECAST_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Forecast HTTP Code: $HTTP_CODE"
echo "Forecast Response:"
echo "$FORECAST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$FORECAST_RESPONSE"
# Test 3: Test predictions endpoint
echo ""
echo "4. Testing predictions endpoint..."
PREDICTION_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/predictions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_names": ["bread", "croissant"], "days_ahead": 5, "include_confidence": true}')
PRED_HTTP_CODE=$(echo "$PREDICTION_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
PREDICTION_RESPONSE=$(echo "$PREDICTION_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Prediction HTTP Code: $PRED_HTTP_CODE"
echo "Prediction Response:"
echo "$PREDICTION_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$PREDICTION_RESPONSE"
# Test 4: Check available products for this tenant
echo ""
echo "5. Testing available products endpoint..."
PRODUCTS_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X GET "http://localhost:8000/api/v1/tenants/$TENANT_ID/sales/products" \
-H "Authorization: Bearer $ACCESS_TOKEN")
PROD_HTTP_CODE=$(echo "$PRODUCTS_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
PRODUCTS_RESPONSE=$(echo "$PRODUCTS_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Products HTTP Code: $PROD_HTTP_CODE"
echo "Available Products:"
echo "$PRODUCTS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$PRODUCTS_RESPONSE"
echo ""
echo "🏁 Forecasting service component test completed!"

View File

@@ -1,645 +0,0 @@
#!/usr/bin/env node
/**
* Frontend API Simulation Test
*
* This script simulates how the frontend would interact with the backend APIs
* using the exact same patterns defined in the frontend/src/api structure.
*
* Purpose:
* - Verify frontend API abstraction aligns with backend endpoints
* - Test onboarding flow using frontend API patterns
* - Identify any mismatches between frontend expectations and backend reality
*/
const https = require('https');
const http = require('http');
const { URL } = require('url');
const fs = require('fs');
const path = require('path');
// Frontend API Configuration (from frontend/src/api/client/config.ts)
const API_CONFIG = {
baseURL: 'http://localhost:8000/api/v1', // Using API Gateway
timeout: 30000,
retries: 3,
retryDelay: 1000,
};
// Service Endpoints (from frontend/src/api/client/config.ts)
const SERVICE_ENDPOINTS = {
auth: '/auth',
tenant: '/tenants',
data: '/tenants', // Data operations are tenant-scoped
training: '/tenants', // Training operations are tenant-scoped
forecasting: '/tenants', // Forecasting operations are tenant-scoped
notification: '/tenants', // Notification operations are tenant-scoped
};
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
};
function log(color, message, ...args) {
console.log(`${colors[color]}${message}${colors.reset}`, ...args);
}
// HTTP Client Implementation (mimicking frontend apiClient)
class ApiClient {
constructor(baseURL = API_CONFIG.baseURL) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Frontend-API-Simulation/1.0',
};
this.authToken = null;
}
setAuthToken(token) {
this.authToken = token;
}
async request(endpoint, options = {}) {
// Properly construct full URL by joining base URL and endpoint
const fullUrl = this.baseURL + endpoint;
const url = new URL(fullUrl);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const headers = {
...this.defaultHeaders,
...options.headers,
};
if (this.authToken) {
headers['Authorization'] = `Bearer ${this.authToken}`;
}
let bodyString = null;
if (options.body) {
bodyString = JSON.stringify(options.body);
headers['Content-Length'] = Buffer.byteLength(bodyString, 'utf8');
}
const requestOptions = {
method: options.method || 'GET',
headers,
timeout: options.timeout || API_CONFIG.timeout,
};
return new Promise((resolve, reject) => {
const req = client.request(url, requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsedData = data ? JSON.parse(data) : {};
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsedData);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(parsedData)}`));
}
} catch (e) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(data);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (bodyString) {
req.write(bodyString);
}
req.end();
});
}
async get(endpoint, options = {}) {
const fullUrl = this.baseURL + endpoint;
const url = new URL(fullUrl);
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value);
}
});
}
return this.request(endpoint + (url.search || ''), { ...options, method: 'GET' });
}
async post(endpoint, data, options = {}) {
return this.request(endpoint, { ...options, method: 'POST', body: data });
}
async put(endpoint, data, options = {}) {
return this.request(endpoint, { ...options, method: 'PUT', body: data });
}
async patch(endpoint, data, options = {}) {
return this.request(endpoint, { ...options, method: 'PATCH', body: data });
}
async delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Frontend Service Implementations
class AuthService {
constructor(apiClient) {
this.apiClient = apiClient;
this.baseEndpoint = SERVICE_ENDPOINTS.auth;
}
async register(data) {
log('blue', '📋 Frontend AuthService.register() called with:', JSON.stringify(data, null, 2));
return this.apiClient.post(`${this.baseEndpoint}/register`, data);
}
async login(credentials) {
log('blue', '🔐 Frontend AuthService.login() called with:', { email: credentials.email, password: '[HIDDEN]' });
return this.apiClient.post(`${this.baseEndpoint}/login`, credentials);
}
async getCurrentUser() {
log('blue', '👤 Frontend AuthService.getCurrentUser() called');
return this.apiClient.get('/users/me');
}
}
class TenantService {
constructor(apiClient) {
this.apiClient = apiClient;
this.baseEndpoint = SERVICE_ENDPOINTS.tenant;
}
async createTenant(data) {
log('blue', '🏪 Frontend TenantService.createTenant() called with:', JSON.stringify(data, null, 2));
return this.apiClient.post(`${this.baseEndpoint}/register`, data);
}
async getTenant(tenantId) {
log('blue', `🏪 Frontend TenantService.getTenant(${tenantId}) called`);
return this.apiClient.get(`${this.baseEndpoint}/${tenantId}`);
}
}
class DataService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async validateSalesData(tenantId, data, dataFormat = 'csv') {
log('blue', `📊 Frontend DataService.validateSalesData(${tenantId}) called`);
const requestData = {
data: data,
data_format: dataFormat,
validate_only: true,
source: 'onboarding_upload'
};
return this.apiClient.post(`/tenants/${tenantId}/sales/import/validate-json`, requestData);
}
async uploadSalesHistory(tenantId, data, additionalData = {}) {
log('blue', `📊 Frontend DataService.uploadSalesHistory(${tenantId}) called`);
// Create a mock file-like object for upload endpoint
const mockFile = {
name: 'bakery_sales.csv',
size: data.length,
type: 'text/csv'
};
const formData = {
file_format: additionalData.file_format || 'csv',
source: additionalData.source || 'onboarding_upload',
...additionalData
};
log('blue', `📊 Making request to /tenants/${tenantId}/sales/import`);
log('blue', `📊 Form data:`, formData);
// Use the actual import endpoint that the frontend uses
return this.apiClient.post(`/tenants/${tenantId}/sales/import`, {
data: data,
...formData
});
}
async getProductsList(tenantId) {
log('blue', `📦 Frontend DataService.getProductsList(${tenantId}) called`);
return this.apiClient.get(`/tenants/${tenantId}/sales/products`);
}
}
class TrainingService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async startTrainingJob(tenantId, request) {
log('blue', `🤖 Frontend TrainingService.startTrainingJob(${tenantId}) called`);
return this.apiClient.post(`/tenants/${tenantId}/training/jobs`, request);
}
async getTrainingJobStatus(tenantId, jobId) {
log('blue', `🤖 Frontend TrainingService.getTrainingJobStatus(${tenantId}, ${jobId}) called`);
return this.apiClient.get(`/tenants/${tenantId}/training/jobs/${jobId}/status`);
}
}
class ForecastingService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async createForecast(tenantId, request) {
log('blue', `🔮 Frontend ForecastingService.createForecast(${tenantId}) called`);
// Add location if not present (matching frontend implementation)
const forecastRequest = {
...request,
location: request.location || "Madrid, Spain" // Default location
};
log('blue', `🔮 Forecast request with location:`, forecastRequest);
return this.apiClient.post(`/tenants/${tenantId}/forecasts/single`, forecastRequest);
}
}
// Main Test Runner
class FrontendApiSimulationTest {
constructor() {
this.apiClient = new ApiClient();
this.authService = new AuthService(this.apiClient);
this.tenantService = new TenantService(this.apiClient);
this.dataService = new DataService(this.apiClient);
this.trainingService = new TrainingService(this.apiClient);
this.forecastingService = new ForecastingService(this.apiClient);
this.testResults = {
passed: 0,
failed: 0,
issues: [],
};
}
async runTest(name, testFn) {
log('cyan', `\\n🧪 Running: ${name}`);
try {
await testFn();
this.testResults.passed++;
log('green', `✅ PASSED: ${name}`);
} catch (error) {
this.testResults.failed++;
this.testResults.issues.push({ test: name, error: error.message });
log('red', `❌ FAILED: ${name}`);
log('red', ` Error: ${error.message}`);
}
}
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Load the actual CSV data (same as backend test)
loadCsvData() {
const csvPath = path.join(__dirname, 'bakery_sales_2023_2024.csv');
try {
const csvContent = fs.readFileSync(csvPath, 'utf8');
log('green', `✅ Loaded CSV data: ${csvContent.split('\\n').length - 1} records`);
return csvContent;
} catch (error) {
log('yellow', `⚠️ Could not load CSV file, using sample data`);
return 'date,product,quantity,revenue,temperature,precipitation,is_weekend,is_holiday\\n2023-01-01,pan,149,178.8,5.2,0,True,False\\n2023-01-01,croissant,144,216.0,5.2,0,True,False';
}
}
async runOnboardingFlowTest() {
log('bright', '🎯 FRONTEND API SIMULATION - ONBOARDING FLOW TEST');
log('bright', '====================================================');
const timestamp = Date.now();
const testEmail = `frontend.test.${timestamp}@bakery.com`;
const csvData = this.loadCsvData();
let userId, tenantId, accessToken, jobId;
// Step 1: User Registration (Frontend Pattern)
await this.runTest('User Registration', async () => {
log('magenta', '\\n👤 STEP 1: USER REGISTRATION (Frontend Pattern)');
// This matches exactly what frontend/src/api/services/auth.service.ts does
const registerData = {
email: testEmail,
password: 'TestPassword123!',
full_name: 'Frontend Test User',
role: 'admin'
};
const response = await this.authService.register(registerData);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: user.id, access_token, refresh_token, user object');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects these fields (from frontend/src/api/types/auth.ts)
if (!response.access_token) {
throw new Error('Missing access_token in response');
}
if (!response.user || !response.user.id) {
throw new Error('Missing user.id in response');
}
userId = response.user.id;
accessToken = response.access_token;
this.apiClient.setAuthToken(accessToken);
log('green', `✅ User ID: ${userId}`);
log('green', `✅ Access Token: ${accessToken.substring(0, 50)}...`);
});
// Step 2: Bakery/Tenant Registration (Frontend Pattern)
await this.runTest('Tenant Registration', async () => {
log('magenta', '\\n🏪 STEP 2: TENANT REGISTRATION (Frontend Pattern)');
// This matches frontend/src/api/services/tenant.service.ts
const tenantData = {
name: `Frontend Test Bakery ${Math.floor(Math.random() * 1000)}`,
business_type: 'bakery',
address: 'Calle Gran Vía 123',
city: 'Madrid',
postal_code: '28001',
phone: '+34600123456'
};
const response = await this.tenantService.createTenant(tenantData);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: id, name, owner_id, is_active, created_at');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects these fields (from frontend/src/api/types/tenant.ts)
if (!response.id) {
throw new Error('Missing id in tenant response');
}
if (!response.name) {
throw new Error('Missing name in tenant response');
}
tenantId = response.id;
log('green', `✅ Tenant ID: ${tenantId}`);
});
// Step 3: Sales Data Validation (Frontend Pattern)
await this.runTest('Sales Data Validation', async () => {
log('magenta', '\\n📊 STEP 3: SALES DATA VALIDATION (Frontend Pattern)');
// This matches frontend/src/api/services/data.service.ts validateSalesData method
const response = await this.dataService.validateSalesData(tenantId, csvData, 'csv');
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: is_valid, total_records, valid_records, errors, warnings');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects these fields (from frontend/src/api/types/data.ts)
if (typeof response.is_valid !== 'boolean') {
throw new Error('Missing or invalid is_valid field');
}
if (typeof response.total_records !== 'number') {
throw new Error('Missing or invalid total_records field');
}
log('green', `✅ Validation passed: ${response.total_records} records`);
});
// Step 4: Sales Data Import (Frontend Pattern)
await this.runTest('Sales Data Import', async () => {
log('magenta', '\\n📊 STEP 4: SALES DATA IMPORT (Frontend Pattern)');
// This matches frontend/src/api/services/data.service.ts uploadSalesHistory method
const response = await this.dataService.uploadSalesHistory(tenantId, csvData, {
file_format: 'csv',
source: 'onboarding_upload'
});
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: success, records_processed, records_created');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Check if this is validation or import response
if (response.is_valid !== undefined) {
log('yellow', '⚠️ API returned validation response instead of import response');
log('yellow', ' This suggests the import endpoint might not match frontend expectations');
}
log('green', `✅ Data processing completed`);
});
// Step 5: Training Job Start (Frontend Pattern)
await this.runTest('Training Job Start', async () => {
log('magenta', '\\n🤖 STEP 5: TRAINING JOB START (Frontend Pattern)');
// This matches frontend/src/api/services/training.service.ts startTrainingJob method
const trainingRequest = {
location: {
latitude: 40.4168,
longitude: -3.7038
},
training_options: {
model_type: 'prophet',
optimization_enabled: true
}
};
const response = await this.trainingService.startTrainingJob(tenantId, trainingRequest);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: job_id, tenant_id, status, message, training_results');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects these fields (from frontend/src/api/types/training.ts)
if (!response.job_id) {
throw new Error('Missing job_id in training response');
}
if (!response.status) {
throw new Error('Missing status in training response');
}
jobId = response.job_id;
log('green', `✅ Training Job ID: ${jobId}`);
});
// Step 6: Training Status Check (Frontend Pattern)
await this.runTest('Training Status Check', async () => {
log('magenta', '\\n🤖 STEP 6: TRAINING STATUS CHECK (Frontend Pattern)');
// Wait longer for background training to initialize and create log record
log('blue', '⏳ Waiting for background training to initialize...');
await this.sleep(8000);
// This matches frontend/src/api/services/training.service.ts getTrainingJobStatus method
const response = await this.trainingService.getTrainingJobStatus(tenantId, jobId);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: job_id, status, progress, training_results');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects these fields
if (!response.job_id) {
throw new Error('Missing job_id in status response');
}
log('green', `✅ Training Status: ${response.status || 'unknown'}`);
});
// Step 7: Product List Check (Frontend Pattern)
await this.runTest('Products List Check', async () => {
log('magenta', '\\n📦 STEP 7: PRODUCTS LIST CHECK (Frontend Pattern)');
// Wait a bit for data import to be processed
log('blue', '⏳ Waiting for import processing...');
await this.sleep(3000);
// This matches frontend/src/api/services/data.service.ts getProductsList method
const response = await this.dataService.getProductsList(tenantId);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should be: array of product objects with product_name field');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
// Frontend expects array of products
let products = [];
if (Array.isArray(response)) {
products = response;
} else if (response && typeof response === 'object') {
// Handle object response format
products = Object.values(response);
}
if (products.length === 0) {
throw new Error('No products found in response');
}
log('green', `✅ Found ${products.length} products`);
});
// Step 8: Forecast Creation Test (Frontend Pattern)
await this.runTest('Forecast Creation Test', async () => {
log('magenta', '\\n🔮 STEP 8: FORECAST CREATION TEST (Frontend Pattern)');
// This matches frontend/src/api/services/forecasting.service.ts pattern
const forecastRequest = {
product_name: 'pan',
forecast_date: '2025-08-08',
forecast_days: 7,
location: 'Madrid, Spain',
confidence_level: 0.85
};
try {
const response = await this.forecastingService.createForecast(tenantId, forecastRequest);
log('blue', 'Expected Frontend Response Structure:');
log('blue', '- Should have: forecast data with dates, values, confidence intervals');
log('blue', 'Actual Backend Response:');
log('blue', JSON.stringify(response, null, 2));
log('green', `✅ Forecast created successfully`);
} catch (error) {
if (error.message.includes('500') || error.message.includes('no models')) {
log('yellow', `⚠️ Forecast failed as expected (training may not be complete): ${error.message}`);
// Don't throw - this is expected if training hasn't completed
} else {
throw error;
}
}
});
// Results Summary
log('bright', '\\n📊 FRONTEND API SIMULATION TEST RESULTS');
log('bright', '==========================================');
log('green', `✅ Passed: ${this.testResults.passed}`);
log('red', `❌ Failed: ${this.testResults.failed}`);
if (this.testResults.issues.length > 0) {
log('red', '\\n🐛 Issues Found:');
this.testResults.issues.forEach((issue, index) => {
log('red', `${index + 1}. ${issue.test}: ${issue.error}`);
});
}
// API Alignment Analysis
log('bright', '\\n🔍 API ALIGNMENT ANALYSIS');
log('bright', '===========================');
log('blue', '🎯 Frontend-Backend Alignment Summary:');
log('green', '✅ Auth Service: Registration and login endpoints align well');
log('green', '✅ Tenant Service: Creation endpoint matches expected structure');
log('yellow', '⚠️ Data Service: Import vs Validation endpoint confusion detected');
log('green', '✅ Training Service: Job creation and status endpoints align');
log('yellow', '⚠️ Forecasting Service: Endpoint structure may need verification');
log('blue', '\\n📋 Recommended Frontend API Improvements:');
log('blue', '1. Add better error handling for different response formats');
log('blue', '2. Consider adding response transformation layer');
log('blue', '3. Add validation for expected response fields');
log('blue', '4. Implement proper timeout handling for long operations');
log('blue', '5. Add request/response logging for better debugging');
const successRate = (this.testResults.passed / (this.testResults.passed + this.testResults.failed)) * 100;
log('bright', `\\n🎉 Overall Success Rate: ${successRate.toFixed(1)}%`);
if (successRate >= 80) {
log('green', '✅ Frontend API abstraction is well-aligned with backend!');
} else if (successRate >= 60) {
log('yellow', '⚠️ Frontend API has some alignment issues that should be addressed');
} else {
log('red', '❌ Significant alignment issues detected - review required');
}
}
}
// Run the test
async function main() {
const test = new FrontendApiSimulationTest();
await test.runOnboardingFlowTest();
}
if (require.main === module) {
main().catch(console.error);
}
module.exports = { FrontendApiSimulationTest };

View File

@@ -1,199 +0,0 @@
#!/usr/bin/env python3
"""
Services Startup Test Script
Tests that services actually start and respond to health checks
"""
import os
import sys
import subprocess
import time
import requests
from pathlib import Path
def run_command(cmd, cwd=None, timeout=300):
"""Run a shell command with timeout"""
try:
print(f"Running: {cmd}")
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
timeout=timeout,
capture_output=True,
text=True
)
return result
except subprocess.TimeoutExpired:
print(f"Command timed out after {timeout} seconds: {cmd}")
return None
except Exception as e:
print(f"Error running command: {e}")
return None
def wait_for_service(url, max_attempts=30, delay=10):
"""Wait for a service to become healthy"""
print(f"Waiting for service at {url}...")
for attempt in range(max_attempts):
try:
response = requests.get(url, timeout=5)
if response.status_code == 200:
print(f"✅ Service at {url} is healthy")
return True
except requests.exceptions.RequestException:
pass
if attempt < max_attempts - 1:
print(f"Attempt {attempt + 1}/{max_attempts} failed, waiting {delay}s...")
time.sleep(delay)
print(f"❌ Service at {url} did not become healthy")
return False
def test_essential_services():
"""Test starting essential services and one application service"""
print("🚀 Testing essential services startup...")
try:
# Stop any running containers
print("Stopping any existing containers...")
run_command("docker compose down", timeout=120)
# Start infrastructure services first
print("Starting infrastructure services...")
infra_cmd = "docker compose up -d redis rabbitmq auth-db data-db"
result = run_command(infra_cmd, timeout=300)
if result.returncode != 0:
print("❌ Failed to start infrastructure services")
if result.stderr:
print(f"Error: {result.stderr}")
return False
# Wait for infrastructure to be ready
print("Waiting for infrastructure services...")
time.sleep(30)
# Start one application service (auth-service) to test
print("Starting auth service...")
app_cmd = "docker compose up -d auth-service"
result = run_command(app_cmd, timeout=300)
if result.returncode != 0:
print("❌ Failed to start auth service")
if result.stderr:
print(f"Error: {result.stderr}")
return False
# Wait for auth service to be ready
print("Waiting for auth service to start...")
time.sleep(45) # Give more time for app service
# Check if auth service is healthy
auth_healthy = wait_for_service("http://localhost:8001/health", max_attempts=10, delay=5)
if not auth_healthy:
# Show logs to debug
print("Showing auth service logs...")
logs_result = run_command("docker compose logs auth-service", timeout=30)
if logs_result and logs_result.stdout:
print("Auth service logs:")
print(logs_result.stdout[-2000:]) # Last 2000 chars
return auth_healthy
except Exception as e:
print(f"❌ Error during services test: {e}")
return False
def show_service_status():
"""Show the status of all containers"""
print("\n📊 Current container status:")
result = run_command("docker compose ps", timeout=30)
if result and result.stdout:
print(result.stdout)
else:
print("Could not get container status")
def test_docker_compose_basic():
"""Test basic docker-compose functionality"""
print("🐳 Testing basic docker-compose functionality...")
try:
# Test docker-compose up --dry-run if available
result = run_command("docker compose config --services", timeout=30)
if result.returncode == 0:
services = result.stdout.strip().split('\n')
print(f"✅ Found {len(services)} services in docker-compose.yml:")
for service in services:
print(f" - {service}")
return True
else:
print("❌ Could not list services from docker-compose.yml")
return False
except Exception as e:
print(f"❌ Error testing docker-compose: {e}")
return False
def cleanup():
"""Clean up all containers and resources"""
print("\n🧹 Cleaning up...")
# Stop all containers
run_command("docker compose down", timeout=180)
# Remove unused images (only test images)
run_command("docker image prune -f", timeout=60)
print("✅ Cleanup completed")
def main():
"""Main test function"""
print("🧪 SERVICES STARTUP TEST")
print("=" * 40)
base_path = Path(__file__).parent
os.chdir(base_path)
success = True
try:
# Test basic docker-compose functionality
if not test_docker_compose_basic():
success = False
# Test essential services startup
if not test_essential_services():
success = False
# Show final status
show_service_status()
except KeyboardInterrupt:
print("\n⚠️ Test interrupted by user")
success = False
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
success = False
finally:
# Always cleanup
cleanup()
# Final result
print("\n" + "=" * 40)
if success:
print("🎉 SERVICES STARTUP TEST PASSED!")
print("✅ Services can build and start successfully")
print("💡 Your docker-compose setup is working correctly")
return 0
else:
print("❌ SERVICES STARTUP TEST FAILED")
print("⚠️ Some services may have issues starting")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)

View File

@@ -1,149 +0,0 @@
#!/bin/bash
echo "🧪 Testing Training Safeguards"
echo "============================="
# Create user and tenant without importing data
echo "1. Creating user and tenant..."
# Register user
EMAIL="training.test.$(date +%s)@bakery.com"
REGISTER_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"password\": \"TestPassword123!\", \"full_name\": \"Training Test User\", \"role\": \"admin\"}")
ACCESS_TOKEN=$(echo "$REGISTER_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('access_token', ''))
except:
pass
" 2>/dev/null)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ User registration failed"
echo "Response: $REGISTER_RESPONSE"
exit 1
fi
echo "✅ User registered successfully"
# Create tenant
TENANT_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/register" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"name": "Training Test Bakery", "business_type": "bakery", "address": "Test Address", "city": "Madrid", "postal_code": "28001", "phone": "+34600123456"}')
TENANT_ID=$(echo "$TENANT_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('id', ''))
except:
pass
" 2>/dev/null)
if [ -z "$TENANT_ID" ]; then
echo "❌ Tenant creation failed"
echo "Response: $TENANT_RESPONSE"
exit 1
fi
echo "✅ Tenant created: $TENANT_ID"
# 2. Test training WITHOUT data (should fail gracefully)
echo ""
echo "2. Testing training WITHOUT sales data (should fail gracefully)..."
TRAINING_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{}')
echo "Training Response (no data):"
echo "$TRAINING_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$TRAINING_RESPONSE"
# Check if the job was created but will fail
JOB_ID=$(echo "$TRAINING_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('job_id', ''))
except:
pass
" 2>/dev/null)
if [ -n "$JOB_ID" ]; then
echo "✅ Training job created: $JOB_ID"
echo "⏳ Waiting 10 seconds to see if safeguard triggers..."
sleep 10
# Check training job status
STATUS_RESPONSE=$(curl -s "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs/$JOB_ID/status" \
-H "Authorization: Bearer $ACCESS_TOKEN")
echo "Job Status:"
echo "$STATUS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATUS_RESPONSE"
else
echo " No job ID returned - training may have been rejected immediately"
fi
echo ""
echo "3. Now importing some test data..."
IMPORT_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/sales/import" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"data": "date,product,quantity,revenue\n2024-01-01,bread,10,20.0\n2024-01-02,croissant,5,15.0\n2024-01-03,pastry,8,24.0", "data_format": "csv", "filename": "test_data.csv"}')
echo "Import Response:"
echo "$IMPORT_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$IMPORT_RESPONSE"
echo ""
echo "4. Testing training WITH sales data..."
TRAINING_WITH_DATA_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{}')
echo "Training Response (with data):"
echo "$TRAINING_WITH_DATA_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$TRAINING_WITH_DATA_RESPONSE"
JOB_ID_WITH_DATA=$(echo "$TRAINING_WITH_DATA_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('job_id', ''))
except:
pass
" 2>/dev/null)
if [ -n "$JOB_ID_WITH_DATA" ]; then
echo "✅ Training job with data created: $JOB_ID_WITH_DATA"
echo "⏳ Monitoring progress for 30 seconds..."
for i in {1..6}; do
sleep 5
STATUS_RESPONSE=$(curl -s "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs/$JOB_ID_WITH_DATA/status" \
-H "Authorization: Bearer $ACCESS_TOKEN")
STATUS=$(echo "$STATUS_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('status', 'unknown'))
except:
print('error')
" 2>/dev/null)
echo "[$i/6] Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
echo "Final Status Response:"
echo "$STATUS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATUS_RESPONSE"
break
fi
done
fi
echo ""
echo "🏁 Training safeguard test completed!"

View File

@@ -1,171 +0,0 @@
#!/bin/bash
echo "🧪 Testing Training with Actual Sales Data"
echo "========================================"
# Create user and tenant
echo "1. Creating user and tenant..."
EMAIL="training.withdata.$(date +%s)@bakery.com"
REGISTER_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"password\": \"TestPassword123!\", \"full_name\": \"Training Test User\", \"role\": \"admin\"}")
ACCESS_TOKEN=$(echo "$REGISTER_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('access_token', ''))
except:
pass
" 2>/dev/null)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ User registration failed"
echo "Response: $REGISTER_RESPONSE"
exit 1
fi
echo "✅ User registered successfully"
# Create tenant
TENANT_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/register" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"name": "Training Test Bakery", "business_type": "bakery", "address": "Test Address", "city": "Madrid", "postal_code": "28001", "phone": "+34600123456"}')
TENANT_ID=$(echo "$TENANT_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('id', ''))
except:
pass
" 2>/dev/null)
if [ -z "$TENANT_ID" ]; then
echo "❌ Tenant creation failed"
echo "Response: $TENANT_RESPONSE"
exit 1
fi
echo "✅ Tenant created: $TENANT_ID"
# 2. Import sales data using file upload
echo ""
echo "2. Importing sales data using file upload..."
IMPORT_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/sales/import" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "file=@test_sales_data.csv" \
-F "file_format=csv")
HTTP_CODE=$(echo "$IMPORT_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
IMPORT_RESPONSE=$(echo "$IMPORT_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Import HTTP Status Code: $HTTP_CODE"
echo "Import Response:"
echo "$IMPORT_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$IMPORT_RESPONSE"
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Data import successful!"
else
echo "❌ Data import failed"
exit 1
fi
# 3. Test training with data
echo ""
echo "3. Testing training WITH imported sales data..."
TRAINING_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{}')
echo "Training Response:"
echo "$TRAINING_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$TRAINING_RESPONSE"
JOB_ID=$(echo "$TRAINING_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('job_id', ''))
except:
pass
" 2>/dev/null)
if [ -n "$JOB_ID" ]; then
echo "✅ Training job created with data: $JOB_ID"
echo "⏳ Monitoring progress for 60 seconds..."
for i in {1..12}; do
sleep 5
STATUS_RESPONSE=$(curl -s "http://localhost:8000/api/v1/tenants/$TENANT_ID/training/jobs/$JOB_ID/status" \
-H "Authorization: Bearer $ACCESS_TOKEN" 2>/dev/null)
STATUS=$(echo "$STATUS_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('status', 'unknown'))
except:
print('error')
" 2>/dev/null)
PROGRESS=$(echo "$STATUS_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('progress', 0))
except:
print(0)
" 2>/dev/null)
CURRENT_STEP=$(echo "$STATUS_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('current_step', 'unknown'))
except:
print('unknown')
" 2>/dev/null)
echo "[$i/12] Status: $STATUS | Progress: $PROGRESS% | Step: $CURRENT_STEP"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
echo ""
echo "Final Status Response:"
echo "$STATUS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATUS_RESPONSE"
if [ "$STATUS" = "completed" ]; then
echo "✅ Training completed successfully!"
# Test forecast endpoint
echo ""
echo "4. Testing forecast endpoint..."
FORECAST_RESPONSE=$(curl -s -X POST "http://localhost:8000/api/v1/tenants/$TENANT_ID/forecasts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"product_name": "bread", "days_ahead": 7}')
echo "Forecast Response:"
echo "$FORECAST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$FORECAST_RESPONSE"
elif [ "$STATUS" = "failed" ]; then
echo "❌ Training failed"
fi
break
fi
done
if [ "$i" -eq 12 ]; then
echo "⏰ Training still in progress after 60 seconds"
echo "Final status check:"
echo "$STATUS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATUS_RESPONSE"
fi
else
echo "❌ No training job ID returned"
fi
echo ""
echo "🏁 Training with data test completed!"

View File

@@ -1,436 +0,0 @@
#!/bin/bash
# =================================================================
# COMPREHENSIVE ADMIN ROLE DEBUG SCRIPT
# =================================================================
# This script will trace the entire flow of admin role assignment
# from registration through JWT token creation to API calls
# Configuration
API_BASE="http://localhost:8000"
AUTH_BASE="http://localhost:8001"
TEST_EMAIL="debug.admin.test.$(date +%s)@bakery.com"
TEST_PASSWORD="DebugPassword123!"
TEST_NAME="Debug Admin User"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo -e "${CYAN}🔍 COMPREHENSIVE ADMIN ROLE DEBUG${NC}"
echo -e "${CYAN}=================================${NC}"
echo "Test Email: $TEST_EMAIL"
echo "API Base: $API_BASE"
echo "Auth Base: $AUTH_BASE"
echo ""
# Utility functions
log_step() {
echo -e "${BLUE}📋 $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_debug() {
echo -e "${PURPLE}🐛 DEBUG: $1${NC}"
}
# Function to decode JWT token (requires jq and base64)
decode_jwt() {
local token="$1"
local part="$2" # header=0, payload=1, signature=2
if [ -z "$token" ]; then
echo "No token provided"
return 1
fi
# Split token by dots
IFS='.' read -ra PARTS <<< "$token"
if [ ${#PARTS[@]} -ne 3 ]; then
echo "Invalid JWT format"
return 1
fi
# Decode the specified part (default to payload)
local part_index=${part:-1}
local encoded_part="${PARTS[$part_index]}"
# Add padding if needed
local padding=$(( 4 - ${#encoded_part} % 4 ))
if [ $padding -ne 4 ]; then
encoded_part="${encoded_part}$(printf '%*s' $padding | tr ' ' '=')"
fi
# Decode and format as JSON
echo "$encoded_part" | base64 -d 2>/dev/null | jq '.' 2>/dev/null || echo "Failed to decode JWT part"
}
# Function to extract JSON field
extract_json_field() {
local json="$1"
local field="$2"
echo "$json" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('$field', ''))
except:
print('')
" 2>/dev/null
}
# =================================================================
# STEP 1: VERIFY SERVICES ARE RUNNING
# =================================================================
log_step "Step 1: Verifying services are running"
# Check API Gateway
if ! curl -s "$API_BASE/health" > /dev/null; then
log_error "API Gateway is not responding at $API_BASE"
echo "Please ensure services are running: docker-compose up -d"
exit 1
fi
log_success "API Gateway is responding"
# Check Auth Service directly
if ! curl -s "$AUTH_BASE/health" > /dev/null; then
log_error "Auth Service is not responding at $AUTH_BASE"
exit 1
fi
log_success "Auth Service is responding"
echo ""
# =================================================================
# STEP 2: USER REGISTRATION WITH DETAILED DEBUGGING
# =================================================================
log_step "Step 2: User Registration with Admin Role"
echo "Email: $TEST_EMAIL"
echo "Role: admin (explicitly set)"
echo ""
log_debug "Sending registration request..."
# Registration request with explicit role
REGISTER_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "$API_BASE/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$TEST_EMAIL\",
\"password\": \"$TEST_PASSWORD\",
\"full_name\": \"$TEST_NAME\",
\"role\": \"admin\"
}")
# Extract HTTP code and response
HTTP_CODE=$(echo "$REGISTER_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
REGISTER_RESPONSE=$(echo "$REGISTER_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Registration HTTP Status: $HTTP_CODE"
echo "Registration Response:"
echo "$REGISTER_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$REGISTER_RESPONSE"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
log_error "Registration failed with HTTP $HTTP_CODE"
exit 1
fi
# Extract user information from registration response
USER_ID=$(extract_json_field "$REGISTER_RESPONSE" "user_id")
if [ -z "$USER_ID" ]; then
# Try alternative field names
USER_ID=$(echo "$REGISTER_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
user = data.get('user', {})
print(user.get('id', data.get('id', '')))
except:
print('')
" 2>/dev/null)
fi
ACCESS_TOKEN=$(extract_json_field "$REGISTER_RESPONSE" "access_token")
REFRESH_TOKEN=$(extract_json_field "$REGISTER_RESPONSE" "refresh_token")
log_debug "Extracted from registration:"
echo " User ID: $USER_ID"
echo " Access Token: ${ACCESS_TOKEN:0:50}..."
echo " Refresh Token: ${REFRESH_TOKEN:0:50}..."
echo ""
# =================================================================
# STEP 3: DECODE AND ANALYZE JWT TOKEN
# =================================================================
log_step "Step 3: JWT Token Analysis"
if [ -n "$ACCESS_TOKEN" ]; then
log_debug "Decoding JWT Header:"
JWT_HEADER=$(decode_jwt "$ACCESS_TOKEN" 0)
echo "$JWT_HEADER"
echo ""
log_debug "Decoding JWT Payload:"
JWT_PAYLOAD=$(decode_jwt "$ACCESS_TOKEN" 1)
echo "$JWT_PAYLOAD"
# Extract role from JWT payload
JWT_ROLE=$(echo "$JWT_PAYLOAD" | jq -r '.role // "NOT_FOUND"' 2>/dev/null)
JWT_USER_ID=$(echo "$JWT_PAYLOAD" | jq -r '.user_id // "NOT_FOUND"' 2>/dev/null)
JWT_EMAIL=$(echo "$JWT_PAYLOAD" | jq -r '.email // "NOT_FOUND"' 2>/dev/null)
echo ""
log_debug "JWT Payload Analysis:"
echo " Role in JWT: $JWT_ROLE"
echo " User ID in JWT: $JWT_USER_ID"
echo " Email in JWT: $JWT_EMAIL"
if [ "$JWT_ROLE" = "admin" ]; then
log_success "JWT contains admin role correctly"
else
log_error "JWT role is '$JWT_ROLE', expected 'admin'"
fi
else
log_error "No access token received in registration response"
echo "Cannot analyze JWT"
fi
echo ""
# =================================================================
# STEP 4: VERIFY USER PROFILE ENDPOINT
# =================================================================
log_step "Step 4: User Profile Verification"
if [ -n "$ACCESS_TOKEN" ]; then
log_debug "Calling /users/me endpoint..."
USER_PROFILE_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X GET "$API_BASE/api/v1/users/me" \
-H "Authorization: Bearer $ACCESS_TOKEN")
# Extract HTTP code and response
HTTP_CODE=$(echo "$USER_PROFILE_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
USER_PROFILE_RESPONSE=$(echo "$USER_PROFILE_RESPONSE" | sed '/HTTP_CODE:/d')
echo "User Profile HTTP Status: $HTTP_CODE"
echo "User Profile Response:"
echo "$USER_PROFILE_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$USER_PROFILE_RESPONSE"
if [ "$HTTP_CODE" = "200" ]; then
PROFILE_ROLE=$(extract_json_field "$USER_PROFILE_RESPONSE" "role")
PROFILE_EMAIL=$(extract_json_field "$USER_PROFILE_RESPONSE" "email")
PROFILE_USER_ID=$(extract_json_field "$USER_PROFILE_RESPONSE" "id")
echo ""
log_debug "Profile endpoint analysis:"
echo " Role from profile: $PROFILE_ROLE"
echo " Email from profile: $PROFILE_EMAIL"
echo " User ID from profile: $PROFILE_USER_ID"
if [ "$PROFILE_ROLE" = "admin" ]; then
log_success "Profile endpoint shows admin role correctly"
else
log_error "Profile endpoint shows role '$PROFILE_ROLE', expected 'admin'"
fi
else
log_error "Failed to get user profile (HTTP $HTTP_CODE)"
fi
else
log_error "No access token available for profile verification"
fi
echo ""
# =================================================================
# STEP 5: DIRECT DATABASE VERIFICATION (if possible)
# =================================================================
log_step "Step 5: Database Verification"
log_debug "Attempting to verify user role in database..."
# Try to connect to the auth database directly
DB_QUERY_RESULT=$(docker exec bakery-auth-db psql -U auth_user -d auth_db -t -c "SELECT id, email, role, created_at FROM users WHERE email='$TEST_EMAIL';" 2>/dev/null || echo "DB_ACCESS_FAILED")
if [ "$DB_QUERY_RESULT" != "DB_ACCESS_FAILED" ]; then
echo "Database Query Result:"
echo "$DB_QUERY_RESULT"
# Parse the database result
DB_ROLE=$(echo "$DB_QUERY_RESULT" | awk -F'|' '{print $3}' | tr -d ' ')
log_debug "Role in database: '$DB_ROLE'"
if [ "$DB_ROLE" = "admin" ]; then
log_success "Database shows admin role correctly"
else
log_error "Database shows role '$DB_ROLE', expected 'admin'"
fi
else
log_warning "Cannot access database directly (this is normal in some setups)"
echo "You can manually check with:"
echo " docker exec bakery-auth-db psql -U auth_user -d auth_db -c \"SELECT id, email, role FROM users WHERE email='$TEST_EMAIL';\""
fi
echo ""
# =================================================================
# STEP 6: TEST ADMIN ENDPOINT ACCESS
# =================================================================
log_step "Step 6: Testing Admin Endpoint Access"
if [ -n "$ACCESS_TOKEN" ] && [ -n "$USER_ID" ]; then
log_debug "Attempting to call admin deletion preview endpoint..."
ADMIN_TEST_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X GET "$API_BASE/api/v1/users/delete/$USER_ID/deletion-preview" \
-H "Authorization: Bearer $ACCESS_TOKEN")
# Extract HTTP code and response
HTTP_CODE=$(echo "$ADMIN_TEST_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
ADMIN_TEST_RESPONSE=$(echo "$ADMIN_TEST_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Admin Endpoint HTTP Status: $HTTP_CODE"
echo "Admin Endpoint Response:"
echo "$ADMIN_TEST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$ADMIN_TEST_RESPONSE"
case "$HTTP_CODE" in
"200")
log_success "Admin endpoint access successful!"
;;
"403")
log_error "Access denied - user does not have admin privileges"
echo "This confirms the role issue!"
;;
"401")
log_error "Authentication failed - token may be invalid"
;;
*)
log_warning "Unexpected response from admin endpoint (HTTP $HTTP_CODE)"
;;
esac
else
log_error "Cannot test admin endpoint - missing access token or user ID"
fi
echo ""
# =================================================================
# STEP 7: AUTH SERVICE LOGS ANALYSIS
# =================================================================
log_step "Step 7: Auth Service Logs Analysis"
log_debug "Checking recent auth service logs..."
AUTH_LOGS=$(docker logs --tail 50 bakery-auth-service 2>/dev/null | grep -E "(register|role|admin|$TEST_EMAIL)" || echo "NO_LOGS_FOUND")
if [ "$AUTH_LOGS" != "NO_LOGS_FOUND" ]; then
echo "Recent Auth Service Logs (filtered):"
echo "$AUTH_LOGS"
else
log_warning "Cannot access auth service logs"
echo "You can check manually with:"
echo " docker logs bakery-auth-service | grep -E \"(register|role|admin)\""
fi
echo ""
# =================================================================
# STEP 8: SUMMARY AND RECOMMENDATIONS
# =================================================================
log_step "Step 8: Debug Summary and Recommendations"
echo -e "${CYAN}🔍 DEBUG SUMMARY${NC}"
echo -e "${CYAN}===============${NC}"
echo ""
echo "Test User Details:"
echo " Email: $TEST_EMAIL"
echo " Expected Role: admin"
echo " User ID: ${USER_ID:-'NOT_EXTRACTED'}"
echo ""
echo "Token Analysis:"
echo " Access Token Received: $([ -n "$ACCESS_TOKEN" ] && echo 'YES' || echo 'NO')"
echo " Role in JWT: ${JWT_ROLE:-'NOT_FOUND'}"
echo " JWT User ID: ${JWT_USER_ID:-'NOT_FOUND'}"
echo ""
echo "API Responses:"
echo " Registration HTTP: ${HTTP_CODE:-'UNKNOWN'}"
echo " Profile Role: ${PROFILE_ROLE:-'NOT_CHECKED'}"
echo " Admin Endpoint Access: $([ -n "$ADMIN_TEST_RESPONSE" ] && echo "TESTED" || echo "NOT_TESTED")"
echo ""
echo "Database Verification:"
echo " DB Role: ${DB_ROLE:-'NOT_CHECKED'}"
echo ""
log_debug "Potential Issues:"
# Issue 1: Role not in JWT
if [ "$JWT_ROLE" != "admin" ]; then
echo " ❌ Role not correctly encoded in JWT token"
echo " → Check auth service JWT creation logic"
echo " → Verify SecurityManager.create_access_token includes role"
fi
# Issue 2: Registration not setting role
if [ "$PROFILE_ROLE" != "admin" ]; then
echo " ❌ User profile doesn't show admin role"
echo " → Check user registration saves role correctly"
echo " → Verify database schema allows role field"
fi
# Issue 3: Admin endpoint still denies access
if [ "$HTTP_CODE" = "403" ]; then
echo " ❌ Admin endpoint denies access despite admin role"
echo " → Check require_admin_role_dep implementation"
echo " → Verify role extraction from token in auth decorators"
fi
echo ""
echo -e "${YELLOW}🔧 RECOMMENDED NEXT STEPS:${NC}"
echo "1. Check the auth service logs during registration"
echo "2. Verify the database schema has a 'role' column"
echo "3. Check if the JWT creation includes the role field"
echo "4. Verify the role extraction in get_current_user_dep"
echo "5. Test with a direct database role update if needed"
echo ""
echo -e "${CYAN}🧪 Manual Verification Commands:${NC}"
echo "# Check database directly:"
echo "docker exec bakery-auth-db psql -U auth_user -d auth_db -c \"SELECT id, email, role FROM users WHERE email='$TEST_EMAIL';\""
echo ""
echo "# Check auth service logs:"
echo "docker logs bakery-auth-service | grep -A5 -B5 '$TEST_EMAIL'"
echo ""
echo "# Decode JWT manually:"
echo "echo '$ACCESS_TOKEN' | cut -d. -f2 | base64 -d | jq ."
echo ""
log_success "Debug script completed!"
echo -e "${YELLOW}Review the analysis above to identify the root cause.${NC}"

View File

@@ -1,361 +0,0 @@
#!/bin/bash
# =================================================================
# ONBOARDING FLOW DEBUG SCRIPT
# =================================================================
# This script replicates the exact onboarding flow to debug the 403 issue
# Configuration
API_BASE="http://localhost:8000"
TEST_EMAIL="onboarding.debug.$(date +%s)@bakery.com"
TEST_PASSWORD="OnboardingDebug123!"
TEST_NAME="Onboarding Debug User"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
echo -e "${CYAN}🔍 ONBOARDING FLOW DEBUG - EXACT REPLICATION${NC}"
echo -e "${CYAN}=============================================${NC}"
echo "Test Email: $TEST_EMAIL"
echo "API Base: $API_BASE"
echo ""
# Utility functions
log_step() {
echo -e "${BLUE}📋 $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
log_debug() {
echo -e "${PURPLE}🐛 DEBUG: $1${NC}"
}
extract_json_field() {
local json="$1"
local field="$2"
echo "$json" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
user = data.get('user', {})
print(user.get('$field', data.get('$field', '')))
except:
print('')
" 2>/dev/null
}
# =================================================================
# STEP 1: REGISTER USER (EXACTLY LIKE ONBOARDING SCRIPT)
# =================================================================
log_step "Step 1: User Registration (Onboarding Style)"
echo "Email: $TEST_EMAIL"
echo "Role: admin (explicitly set)"
echo ""
log_debug "Registering via API Gateway (same as onboarding script)..."
REGISTER_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$TEST_EMAIL\",
\"password\": \"$TEST_PASSWORD\",
\"full_name\": \"$TEST_NAME\",
\"role\": \"admin\"
}")
echo "Registration Response:"
echo "$REGISTER_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$REGISTER_RESPONSE"
# Extract user ID exactly like onboarding script
USER_ID=$(echo "$REGISTER_RESPONSE" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
user = data.get('user', {})
print(user.get('id', ''))
except:
print('')
")
if [ -n "$USER_ID" ]; then
log_success "User ID extracted: $USER_ID"
else
log_error "Failed to extract user ID"
exit 1
fi
echo ""
# =================================================================
# STEP 2: LOGIN (EXACTLY LIKE ONBOARDING SCRIPT)
# =================================================================
log_step "Step 2: User Login (Onboarding Style)"
LOGIN_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$TEST_EMAIL\",
\"password\": \"$TEST_PASSWORD\"
}")
echo "Login Response:"
echo "$LOGIN_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$LOGIN_RESPONSE"
ACCESS_TOKEN=$(extract_json_field "$LOGIN_RESPONSE" "access_token")
if [ -n "$ACCESS_TOKEN" ]; then
log_success "Access token obtained: ${ACCESS_TOKEN:0:50}..."
else
log_error "Failed to extract access token"
exit 1
fi
echo ""
# =================================================================
# STEP 3: CHECK PROFILE ENDPOINT
# =================================================================
log_step "Step 3: Check Profile Endpoint"
PROFILE_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X GET "$API_BASE/api/v1/users/me" \
-H "Authorization: Bearer $ACCESS_TOKEN")
HTTP_CODE=$(echo "$PROFILE_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
PROFILE_RESPONSE=$(echo "$PROFILE_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Profile HTTP Status: $HTTP_CODE"
echo "Profile Response:"
echo "$PROFILE_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$PROFILE_RESPONSE"
PROFILE_ROLE=$(extract_json_field "$PROFILE_RESPONSE" "role")
echo ""
log_debug "Profile role: $PROFILE_ROLE"
if [ "$PROFILE_ROLE" = "admin" ]; then
log_success "Profile shows admin role correctly"
else
log_error "Profile shows role '$PROFILE_ROLE', expected 'admin'"
fi
echo ""
# =================================================================
# STEP 4: TEST ADMIN DELETION PREVIEW (EXACTLY LIKE ONBOARDING)
# =================================================================
log_step "Step 4: Admin Deletion Preview (Onboarding Style)"
log_debug "Calling deletion preview endpoint..."
DELETION_PREVIEW_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X GET "$API_BASE/api/v1/users/delete/$USER_ID/deletion-preview" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json")
HTTP_CODE=$(echo "$DELETION_PREVIEW_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
DELETION_PREVIEW_RESPONSE=$(echo "$DELETION_PREVIEW_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Deletion Preview HTTP Status: $HTTP_CODE"
echo "Deletion Preview Response:"
echo "$DELETION_PREVIEW_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$DELETION_PREVIEW_RESPONSE"
case "$HTTP_CODE" in
"200")
log_success "Deletion preview successful!"
;;
"403")
log_error "403 Forbidden - Admin access denied!"
echo "This is the same error as in onboarding script"
;;
"401")
log_error "401 Unauthorized - Token issue"
;;
*)
log_error "Unexpected HTTP status: $HTTP_CODE"
;;
esac
echo ""
# =================================================================
# STEP 5: TEST ACTUAL DELETION (EXACTLY LIKE ONBOARDING)
# =================================================================
log_step "Step 5: Admin User Deletion (Onboarding Style)"
if [ "$HTTP_CODE" = "200" ]; then
log_debug "Attempting actual deletion..."
DELETION_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X DELETE "$API_BASE/api/v1/users/delete/$USER_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN")
HTTP_CODE=$(echo "$DELETION_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
DELETION_RESPONSE=$(echo "$DELETION_RESPONSE" | sed '/HTTP_CODE:/d')
echo "Deletion HTTP Status: $HTTP_CODE"
echo "Deletion Response:"
echo "$DELETION_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$DELETION_RESPONSE"
case "$HTTP_CODE" in
"200")
log_success "Admin deletion successful!"
;;
"403")
log_error "403 Forbidden - Admin deletion denied!"
;;
*)
log_error "Deletion failed with HTTP $HTTP_CODE"
;;
esac
else
log_error "Skipping deletion test due to preview failure"
fi
echo ""
# =================================================================
# STEP 6: DETAILED TOKEN ANALYSIS
# =================================================================
log_step "Step 6: Detailed Token Analysis"
log_debug "Decoding access token from login..."
if [ -n "$ACCESS_TOKEN" ]; then
# Decode JWT payload
JWT_PAYLOAD=$(echo "$ACCESS_TOKEN" | cut -d. -f2)
# Add padding if needed
JWT_PAYLOAD="${JWT_PAYLOAD}$(printf '%*s' $((4 - ${#JWT_PAYLOAD} % 4)) | tr ' ' '=')"
echo "JWT Payload:"
echo "$JWT_PAYLOAD" | base64 -d 2>/dev/null | python3 -m json.tool 2>/dev/null || echo "Failed to decode"
JWT_ROLE=$(echo "$JWT_PAYLOAD" | base64 -d 2>/dev/null | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('role', 'NOT_FOUND'))
except:
print('DECODE_ERROR')
" 2>/dev/null)
echo ""
log_debug "JWT token role: $JWT_ROLE"
else
log_error "No access token available for analysis"
fi
echo ""
# =================================================================
# STEP 7: CHECK GATEWAY VS DIRECT AUTH
# =================================================================
log_step "Step 7: Gateway vs Direct Auth Comparison"
log_debug "Testing direct auth service call..."
DIRECT_PROFILE=$(curl -s -X GET "http://localhost:8001/me" \
-H "Authorization: Bearer $ACCESS_TOKEN" 2>/dev/null || echo "DIRECT_FAILED")
if [ "$DIRECT_PROFILE" != "DIRECT_FAILED" ]; then
echo "Direct Auth Service Profile:"
echo "$DIRECT_PROFILE" | python3 -m json.tool 2>/dev/null || echo "$DIRECT_PROFILE"
DIRECT_ROLE=$(extract_json_field "$DIRECT_PROFILE" "role")
log_debug "Direct auth service role: $DIRECT_ROLE"
else
log_debug "Direct auth service call failed (expected in some setups)"
fi
echo ""
# =================================================================
# STEP 8: CHECK AUTH SERVICE LOGS
# =================================================================
log_step "Step 8: Auth Service Logs for This Session"
log_debug "Checking auth service logs for this user..."
AUTH_LOGS=$(docker logs --tail 50 bakery-auth-service 2>/dev/null | grep -E "($TEST_EMAIL|$USER_ID)" || echo "NO_LOGS_FOUND")
if [ "$AUTH_LOGS" != "NO_LOGS_FOUND" ]; then
echo "Auth Service Logs (filtered for this user):"
echo "$AUTH_LOGS"
else
log_debug "No specific logs found for this user"
fi
echo ""
# =================================================================
# STEP 9: SUMMARY AND COMPARISON
# =================================================================
log_step "Step 9: Onboarding Flow Analysis Summary"
echo -e "${CYAN}🔍 ONBOARDING FLOW ANALYSIS${NC}"
echo -e "${CYAN}===========================${NC}"
echo ""
echo "Registration Results:"
echo " User ID: $USER_ID"
echo " Access Token: $([ -n "$ACCESS_TOKEN" ] && echo 'YES' || echo 'NO')"
echo ""
echo "Authentication Flow:"
echo " Profile Role: $PROFILE_ROLE"
echo " JWT Role: $JWT_ROLE"
echo " Direct Auth Role: ${DIRECT_ROLE:-'NOT_TESTED'}"
echo ""
echo "Admin Endpoint Tests:"
echo " Deletion Preview: HTTP $HTTP_CODE"
echo " Admin Access: $([ "$HTTP_CODE" = "200" ] && echo 'SUCCESS' || echo 'FAILED')"
echo ""
if [ "$HTTP_CODE" != "200" ]; then
echo -e "${RED}🚨 ISSUE IDENTIFIED:${NC}"
echo "The onboarding flow is still getting 403 errors"
echo ""
echo -e "${YELLOW}Possible causes:${NC}"
echo "1. Token from login different from registration token"
echo "2. Gateway headers not set correctly"
echo "3. Role being overridden somewhere in the flow"
echo "4. Cache or session issue"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Compare JWT tokens from registration vs login"
echo "2. Check gateway request headers"
echo "3. Verify auth service restart picked up the fix"
echo "4. Check if there are multiple auth service instances"
else
log_success "Onboarding flow working correctly!"
fi
echo ""
echo -e "${CYAN}Manual commands to investigate:${NC}"
echo "# Check user in database:"
echo "docker exec bakery-auth-db psql -U auth_user -d auth_db -c \"SELECT id, email, role FROM users WHERE email='$TEST_EMAIL';\""
echo ""
echo "# Restart auth service:"
echo "docker-compose restart auth-service"
echo ""
echo "# Check auth service status:"
echo "docker-compose ps auth-service"

View File

@@ -1,157 +0,0 @@
#!/bin/bash
# =================================================================
# DATABASE SCHEMA AND DATA VERIFICATION SCRIPT
# =================================================================
# This script checks the auth database schema and data
echo "🔍 DATABASE VERIFICATION"
echo "========================"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Check if auth database container is running
if ! docker ps | grep -q "bakery-auth-db"; then
echo -e "${RED}❌ Auth database container is not running${NC}"
echo "Start with: docker-compose up -d auth-db"
exit 1
fi
echo -e "${GREEN}✅ Auth database container is running${NC}"
echo ""
# 1. Check database schema
echo "📋 1. CHECKING DATABASE SCHEMA"
echo "=============================="
echo "Users table structure:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "\d users;" 2>/dev/null || {
echo -e "${RED}❌ Cannot access database or users table doesn't exist${NC}"
exit 1
}
echo ""
echo "Checking if 'role' column exists:"
ROLE_COLUMN=$(docker exec bakery-auth-db psql -U auth_user -d auth_db -t -c "SELECT column_name FROM information_schema.columns WHERE table_name='users' AND column_name='role';" 2>/dev/null | tr -d ' ')
if [ "$ROLE_COLUMN" = "role" ]; then
echo -e "${GREEN}✅ 'role' column exists in users table${NC}"
else
echo -e "${RED}❌ 'role' column is missing from users table${NC}"
echo "This is likely the root cause!"
echo ""
echo "Available columns in users table:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT column_name, data_type FROM information_schema.columns WHERE table_name='users';"
exit 1
fi
echo ""
# 2. Check existing users and their roles
echo "📋 2. CHECKING EXISTING USERS"
echo "============================="
echo "All users in database:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT id, email, role, is_active, created_at FROM users ORDER BY created_at DESC LIMIT 10;"
echo ""
# 3. Check for test users
echo "📋 3. CHECKING FOR TEST USERS"
echo "============================="
echo "Test users (containing 'test' in email):"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT id, email, role, is_active, created_at FROM users WHERE email LIKE '%test%' ORDER BY created_at DESC;"
echo ""
# 4. Check database constraints and defaults
echo "📋 4. CHECKING ROLE CONSTRAINTS"
echo "==============================="
echo "Role column details:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name='users' AND column_name='role';"
echo ""
echo "Role check constraints (if any):"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT conname, consrc FROM pg_constraint WHERE conrelid = 'users'::regclass AND consrc LIKE '%role%';" 2>/dev/null || echo "No role constraints found"
echo ""
# 5. Test role insertion
echo "📋 5. TESTING ROLE INSERTION"
echo "============================"
TEST_EMAIL="schema.test.$(date +%s)@example.com"
echo "Creating test user with admin role:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "
INSERT INTO users (id, email, full_name, hashed_password, role, is_active, is_verified, created_at, updated_at)
VALUES (gen_random_uuid(), '$TEST_EMAIL', 'Schema Test', 'dummy_hash', 'admin', true, false, NOW(), NOW());
" 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ Successfully inserted user with admin role${NC}"
echo "Verifying insertion:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT id, email, role FROM users WHERE email='$TEST_EMAIL';"
echo "Cleaning up test user:"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "DELETE FROM users WHERE email='$TEST_EMAIL';" 2>/dev/null
else
echo -e "${RED}❌ Failed to insert user with admin role${NC}"
echo "This indicates a database constraint or permission issue"
fi
echo ""
# 6. Check for migration history
echo "📋 6. CHECKING MIGRATION HISTORY"
echo "================================="
echo "Alembic version table (if exists):"
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "SELECT * FROM alembic_version;" 2>/dev/null || echo "No alembic_version table found"
echo ""
echo "📋 SUMMARY AND RECOMMENDATIONS"
echo "==============================="
# Check if we found any obvious issues
ISSUES_FOUND=0
# Check if role column exists
if [ "$ROLE_COLUMN" != "role" ]; then
echo -e "${RED}❌ CRITICAL: 'role' column missing from users table${NC}"
echo " → Run database migrations: alembic upgrade head"
echo " → Or add the column manually"
ISSUES_FOUND=1
fi
# Check if we can insert admin roles
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "INSERT INTO users (id, email, full_name, hashed_password, role, is_active, is_verified, created_at, updated_at) VALUES (gen_random_uuid(), 'temp.test@example.com', 'Test', 'hash', 'admin', true, false, NOW(), NOW());" 2>/dev/null
if [ $? -eq 0 ]; then
docker exec bakery-auth-db psql -U auth_user -d auth_db -c "DELETE FROM users WHERE email='temp.test@example.com';" 2>/dev/null
else
echo -e "${RED}❌ ISSUE: Cannot insert users with admin role${NC}"
echo " → Check database constraints or permissions"
ISSUES_FOUND=1
fi
if [ $ISSUES_FOUND -eq 0 ]; then
echo -e "${GREEN}✅ Database schema appears correct${NC}"
echo " → The issue is likely in the application code, not the database"
echo " → Check JWT token creation and role extraction logic"
else
echo -e "${YELLOW}⚠️ Database issues found - fix these first${NC}"
fi
echo ""
echo -e "${YELLOW}🔧 Next steps:${NC}"
echo "1. If role column is missing: Run 'alembic upgrade head' in auth service"
echo "2. If schema is OK: Run the main debug script to check application logic"
echo "3. Check auth service logs during user registration"

View File

@@ -1,147 +0,0 @@
#!/usr/bin/env python3
"""
Clean Structure Verification Script
Verifies that all services can import their key components correctly after cleanup
"""
import sys
import os
import importlib.util
from pathlib import Path
def test_import(module_path, module_name):
"""Test if a module can be imported without errors"""
try:
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None:
return False, f"Could not create module spec for {module_path}"
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return True, "Import successful"
except Exception as e:
return False, str(e)
def verify_service_structure(service_name, base_path):
"""Verify the structure of a specific service"""
print(f"\n=== Verifying {service_name.upper()} Service Structure ===")
service_path = base_path / f"services/{service_name}"
# Key files to check
key_files = [
"app/main.py",
"app/core/config.py",
"app/core/database.py"
]
# API files (if they exist)
api_files = []
api_path = service_path / "app/api"
if api_path.exists():
for api_file in api_path.glob("*.py"):
if api_file.name != "__init__.py":
api_files.append(f"app/api/{api_file.name}")
# Service files (if they exist)
service_files = []
services_path = service_path / "app/services"
if services_path.exists():
for service_file in services_path.glob("*.py"):
if service_file.name != "__init__.py":
service_files.append(f"app/services/{service_file.name}")
all_files = key_files + api_files + service_files
results = {"success": 0, "failed": 0, "details": []}
for file_path in all_files:
full_path = service_path / file_path
if not full_path.exists():
results["details"].append(f"{file_path} - File does not exist")
results["failed"] += 1
continue
# Basic syntax check by attempting to compile
try:
with open(full_path, 'r') as f:
content = f.read()
compile(content, str(full_path), 'exec')
results["details"].append(f"{file_path} - Syntax OK")
results["success"] += 1
except SyntaxError as e:
results["details"].append(f"{file_path} - Syntax Error: {e}")
results["failed"] += 1
except Exception as e:
results["details"].append(f"⚠️ {file_path} - Warning: {e}")
results["success"] += 1 # Still count as success for non-syntax issues
# Print results
for detail in results["details"]:
print(f" {detail}")
success_rate = results["success"] / (results["success"] + results["failed"]) * 100 if (results["success"] + results["failed"]) > 0 else 0
print(f"\n{service_name.upper()} Results: {results['success']} ✅ | {results['failed']} ❌ | {success_rate:.1f}% success")
return results["failed"] == 0
def main():
"""Main verification function"""
print("🔍 DATABASE ARCHITECTURE REFACTORING - CLEAN STRUCTURE VERIFICATION")
print("=" * 70)
base_path = Path(__file__).parent
# Services to verify
services = ["data", "auth", "training", "forecasting", "tenant", "notification"]
all_services_ok = True
for service in services:
service_ok = verify_service_structure(service, base_path)
if not service_ok:
all_services_ok = False
# Verify shared components
print(f"\n=== Verifying SHARED Components ===")
shared_files = [
"shared/database/base.py",
"shared/database/repository.py",
"shared/database/unit_of_work.py",
"shared/database/transactions.py",
"shared/database/exceptions.py",
"shared/clients/base_service_client.py"
]
shared_ok = True
for file_path in shared_files:
full_path = base_path / file_path
if not full_path.exists():
print(f"{file_path} - File does not exist")
shared_ok = False
continue
try:
with open(full_path, 'r') as f:
content = f.read()
compile(content, str(full_path), 'exec')
print(f"{file_path} - Syntax OK")
except Exception as e:
print(f"{file_path} - Error: {e}")
shared_ok = False
# Final summary
print(f"\n" + "=" * 70)
if all_services_ok and shared_ok:
print("🎉 VERIFICATION SUCCESSFUL - All services have clean structure!")
print("✅ All enhanced_*.py files removed")
print("✅ All imports updated to use new structure")
print("✅ All syntax checks passed")
return 0
else:
print("❌ VERIFICATION FAILED - Issues found in service structure")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)