Improve backend

This commit is contained in:
Urtzi Alfaro
2025-11-18 07:17:17 +01:00
parent d36f2ab9af
commit 5c45164c8e
61 changed files with 9846 additions and 495 deletions

View File

@@ -37,6 +37,7 @@ import { OrchestrationSummaryCard } from '../../components/dashboard/Orchestrati
import { ProductionTimelineCard } from '../../components/dashboard/ProductionTimelineCard';
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
import { PurchaseOrderDetailsModal } from '../../components/dashboard/PurchaseOrderDetailsModal';
import { ModifyPurchaseOrderModal } from '../../components/domain/procurement/ModifyPurchaseOrderModal';
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
import type { ItemType } from '../../components/domain/unified-wizard';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
@@ -57,6 +58,10 @@ export function NewDashboardPage() {
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
// PO Modify Modal state
const [modifyPOId, setModifyPOId] = useState<string | null>(null);
const [isModifyPOModalOpen, setIsModifyPOModalOpen] = useState(false);
// Data fetching
const {
data: healthStatus,
@@ -124,8 +129,9 @@ export function NewDashboardPage() {
};
const handleModify = (actionId: string) => {
// Navigate to procurement page for modification
navigate(`/app/operations/procurement`);
// Open modal to modify PO
setModifyPOId(actionId);
setIsModifyPOModalOpen(true);
};
const handleStartBatch = async (batchId: string) => {
@@ -209,7 +215,7 @@ export function NewDashboardPage() {
}, [isDemoMode, startTour]);
return (
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="min-h-screen pb-20 md:pb-8">
{/* Mobile-optimized container */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Header */}
@@ -307,7 +313,7 @@ export function NewDashboardPage() {
</div>
{/* SECTION 6: Quick Action Links */}
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<button
@@ -374,6 +380,23 @@ export function NewDashboardPage() {
onModify={handleModify}
/>
)}
{/* Modify Purchase Order Modal */}
{modifyPOId && (
<ModifyPurchaseOrderModal
poId={modifyPOId}
isOpen={isModifyPOModalOpen}
onClose={() => {
setIsModifyPOModalOpen(false);
setModifyPOId(null);
}}
onSuccess={() => {
setIsModifyPOModalOpen(false);
setModifyPOId(null);
handleRefreshAll();
}}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,296 @@
// frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
/**
* WhatsApp Admin Management Page
* Admin-only interface for assigning WhatsApp phone numbers to tenants
*/
import React, { useState, useEffect } from 'react';
import { MessageSquare, Phone, CheckCircle, AlertCircle, Loader2, Users } from 'lucide-react';
import axios from 'axios';
interface PhoneNumberInfo {
id: string;
display_phone_number: string;
verified_name: string;
quality_rating: string;
}
interface TenantWhatsAppStatus {
tenant_id: string;
tenant_name: string;
whatsapp_enabled: boolean;
phone_number_id: string | null;
display_phone_number: string | null;
}
const WhatsAppAdminPage: React.FC = () => {
const [availablePhones, setAvailablePhones] = useState<PhoneNumberInfo[]>([]);
const [tenants, setTenants] = useState<TenantWhatsAppStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [assigningPhone, setAssigningPhone] = useState<string | null>(null);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001';
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
// Fetch available phone numbers
const phonesResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/phone-numbers`);
setAvailablePhones(phonesResponse.data);
// Fetch tenant WhatsApp status
const tenantsResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants`);
setTenants(tenantsResponse.data);
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to load WhatsApp data');
console.error('Failed to fetch WhatsApp data:', err);
} finally {
setLoading(false);
}
};
const assignPhoneNumber = async (tenantId: string, phoneNumberId: string, displayPhone: string) => {
setAssigningPhone(tenantId);
try {
await axios.post(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/assign-phone`, {
phone_number_id: phoneNumberId,
display_phone_number: displayPhone
});
// Refresh data
await fetchData();
} catch (err: any) {
alert(err.response?.data?.detail || 'Failed to assign phone number');
console.error('Failed to assign phone:', err);
} finally {
setAssigningPhone(null);
}
};
const unassignPhoneNumber = async (tenantId: string) => {
if (!confirm('Are you sure you want to unassign this phone number?')) {
return;
}
setAssigningPhone(tenantId);
try {
await axios.delete(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/unassign-phone`);
// Refresh data
await fetchData();
} catch (err: any) {
alert(err.response?.data?.detail || 'Failed to unassign phone number');
console.error('Failed to unassign phone:', err);
} finally {
setAssigningPhone(null);
}
};
const getQualityRatingColor = (rating: string) => {
switch (rating.toUpperCase()) {
case 'GREEN':
return 'text-green-600 bg-green-100';
case 'YELLOW':
return 'text-yellow-600 bg-yellow-100';
case 'RED':
return 'text-red-600 bg-red-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<MessageSquare className="w-8 h-8 text-blue-600" />
WhatsApp Admin Management
</h1>
<p className="text-gray-600 mt-2">
Assign WhatsApp phone numbers to bakery tenants
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-800">{error}</div>
</div>
)}
{/* Available Phone Numbers */}
<div className="mb-8 bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-200 bg-gray-50">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Phone className="w-5 h-5" />
Available Phone Numbers ({availablePhones.length})
</h2>
</div>
<div className="divide-y divide-gray-200">
{availablePhones.length === 0 ? (
<div className="p-6 text-center text-gray-500">
<p>No phone numbers available. Please add phone numbers to your WhatsApp Business Account.</p>
</div>
) : (
availablePhones.map((phone) => (
<div key={phone.id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Phone className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-mono font-semibold text-gray-900">{phone.display_phone_number}</p>
<p className="text-sm text-gray-500">{phone.verified_name}</p>
<p className="text-xs text-gray-400 mt-1">ID: {phone.id}</p>
</div>
</div>
<div className={`px-3 py-1 rounded-full text-xs font-semibold ${getQualityRatingColor(phone.quality_rating)}`}>
{phone.quality_rating}
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Tenants List */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-200 bg-gray-50">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Users className="w-5 h-5" />
Bakery Tenants ({tenants.length})
</h2>
</div>
<div className="divide-y divide-gray-200">
{tenants.length === 0 ? (
<div className="p-6 text-center text-gray-500">
<p>No tenants found.</p>
</div>
) : (
tenants.map((tenant) => (
<div key={tenant.tenant_id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-gray-900">{tenant.tenant_name}</h3>
{tenant.whatsapp_enabled && tenant.display_phone_number ? (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
<CheckCircle className="w-3 h-3" />
Active
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
<AlertCircle className="w-3 h-3" />
Not Configured
</span>
)}
</div>
{tenant.display_phone_number ? (
<p className="text-sm text-gray-600 mt-1 font-mono">
Phone: {tenant.display_phone_number}
</p>
) : (
<p className="text-sm text-gray-500 mt-1">
No phone number assigned
</p>
)}
</div>
<div className="flex items-center gap-2">
{tenant.display_phone_number ? (
<button
onClick={() => unassignPhoneNumber(tenant.tenant_id)}
disabled={assigningPhone === tenant.tenant_id}
className="px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{assigningPhone === tenant.tenant_id ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Unassigning...
</>
) : (
'Unassign'
)}
</button>
) : (
<div className="flex items-center gap-2">
<select
onChange={(e) => {
if (e.target.value) {
const phone = availablePhones.find(p => p.id === e.target.value);
if (phone) {
assignPhoneNumber(tenant.tenant_id, phone.id, phone.display_phone_number);
}
e.target.value = ''; // Reset select
}
}}
disabled={assigningPhone === tenant.tenant_id || availablePhones.length === 0}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="">Assign phone number...</option>
{availablePhones.map((phone) => (
<option key={phone.id} value={phone.id}>
{phone.display_phone_number} - {phone.verified_name}
</option>
))}
</select>
{assigningPhone === tenant.tenant_id && (
<Loader2 className="w-5 h-5 animate-spin text-blue-600" />
)}
</div>
)}
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Refresh Button */}
<div className="mt-6 flex justify-end">
<button
onClick={fetchData}
disabled={loading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 font-medium"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Refreshing...
</>
) : (
'Refresh Data'
)}
</button>
</div>
</div>
</div>
);
};
export default WhatsAppAdminPage;

View File

@@ -44,12 +44,6 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
onChange({ ...settings, [field]: newChannels });
};
const apiVersionOptions = [
{ value: 'v18.0', label: 'v18.0' },
{ value: 'v19.0', label: 'v19.0' },
{ value: 'v20.0', label: 'v20.0' }
];
const languageOptions = [
{ value: 'es', label: 'Español' },
{ value: 'eu', label: 'Euskara' },
@@ -80,45 +74,40 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
<>
<div className="p-4 sm:p-6 bg-[var(--bg-secondary)]">
<h5 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
WhatsApp Business API Configuration
WhatsApp Configuration
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t('notification.whatsapp_phone_number_id')}
value={settings.whatsapp_phone_number_id}
onChange={handleChange('whatsapp_phone_number_id')}
disabled={disabled}
placeholder="123456789012345"
helperText={t('notification.whatsapp_phone_number_id_help')}
/>
<Input
type="password"
label={t('notification.whatsapp_access_token')}
value={settings.whatsapp_access_token}
onChange={handleChange('whatsapp_access_token')}
disabled={disabled}
placeholder="EAAxxxxxxxx"
helperText={t('notification.whatsapp_access_token_help')}
/>
<Input
label={t('notification.whatsapp_business_account_id')}
value={settings.whatsapp_business_account_id}
onChange={handleChange('whatsapp_business_account_id')}
disabled={disabled}
placeholder="987654321098765"
helperText={t('notification.whatsapp_business_account_id_help')}
/>
<Select
label={t('notification.whatsapp_api_version')}
options={apiVersionOptions}
value={settings.whatsapp_api_version}
onChange={handleSelectChange('whatsapp_api_version')}
disabled={disabled}
/>
{/* Display Phone Number */}
{settings.whatsapp_display_phone_number ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 bg-green-100 dark:bg-green-800 rounded-full">
<MessageSquare className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-green-900 dark:text-green-100">
WhatsApp Configured
</p>
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
Phone: <span className="font-mono font-semibold">{settings.whatsapp_display_phone_number}</span>
</p>
</div>
</div>
</div>
) : (
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
<div className="text-xs text-yellow-700 dark:text-yellow-300">
<p className="font-semibold mb-1">No phone number assigned</p>
<p>Please contact support to have a WhatsApp phone number assigned to your bakery.</p>
</div>
</div>
</div>
)}
{/* Language Preference */}
<div className="mt-4">
<Select
label={t('notification.whatsapp_default_language')}
options={languageOptions}
@@ -128,17 +117,13 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
/>
</div>
{/* WhatsApp Setup Info */}
{/* WhatsApp Info */}
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div className="text-xs text-blue-700 dark:text-blue-300">
<p className="font-semibold mb-1">{t('notification.whatsapp_setup_note')}</p>
<ul className="list-disc list-inside space-y-1">
<li>{t('notification.whatsapp_setup_step1')}</li>
<li>{t('notification.whatsapp_setup_step2')}</li>
<li>{t('notification.whatsapp_setup_step3')}</li>
</ul>
<p className="font-semibold mb-1">WhatsApp Notifications Included</p>
<p>WhatsApp messaging is included in your subscription. Your notifications will be sent from the phone number shown above to your suppliers and team members.</p>
</div>
</div>
</div>

View File

@@ -21,7 +21,8 @@ import {
Building2,
Cloud,
Euro,
ChevronRight
ChevronRight,
Play
} from 'lucide-react';
const LandingPage: React.FC = () => {
@@ -90,6 +91,18 @@ const LandingPage: React.FC = () => {
</span>
</Button>
</Link>
<Link to={getDemoUrl()} className="w-full sm:w-auto">
<Button
size="lg"
variant="secondary"
className="w-full sm:w-auto group px-10 py-5 text-lg font-bold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 rounded-xl"
>
<span className="flex items-center justify-center gap-2">
{t('landing:hero.cta_demo', 'Ver Demo')}
<Play className="w-5 h-5 group-hover:scale-110 transition-transform" />
</span>
</Button>
</Link>
</div>
{/* Social Proof - New */}
@@ -98,13 +111,13 @@ const LandingPage: React.FC = () => {
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-[var(--text-secondary)]">
<AnimatedCounter value={20} className="inline font-bold" /> panaderías ya ahorran <AnimatedCounter value={1500} prefix="€" className="inline font-bold" />/mes de promedio
{t('landing:hero.social_proof.bakeries', '20 panaderías ya ahorran €1,500/mes de promedio')}
</span>
</div>
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
<Target className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-[var(--text-secondary)]">
Predicciones <AnimatedCounter value={92} suffix="%" className="inline font-bold" /> precisas (vs 60% sistemas genéricos)
{t('landing:hero.social_proof.accuracy', 'Predicciones 92% precisas (vs 60% sistemas genéricos)')}
</span>
</div>
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">