Improve backend
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal file
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]">
|
||||
|
||||
Reference in New Issue
Block a user