Add whatsapp feature

This commit is contained in:
Urtzi Alfaro
2025-11-13 16:01:08 +01:00
parent d7df2b0853
commit 9bc048d360
74 changed files with 9765 additions and 533 deletions

View File

@@ -18,6 +18,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { RefreshCw, ExternalLink, Plus, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useTenant } from '../../stores/tenant.store';
import {
useBakeryHealthStatus,
@@ -38,10 +39,10 @@ import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
import type { ItemType } from '../../components/domain/unified-wizard';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
import { DemoBanner } from '../../components/layout/DemoBanner/DemoBanner';
export function NewDashboardPage() {
const navigate = useNavigate();
const { t } = useTranslation(['dashboard', 'common']);
const { currentTenant } = useTenant();
const tenantId = currentTenant?.id || '';
const { startTour } = useDemoTour();
@@ -188,16 +189,13 @@ export function NewDashboardPage() {
return (
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{/* Demo Banner */}
{isDemoMode && <DemoBanner />}
{/* Mobile-optimized container */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl md:text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>Panel de Control</h1>
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>Your bakery at a glance</p>
<h1 className="text-3xl md:text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:title')}</h1>
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>{t('dashboard:subtitle')}</p>
</div>
{/* Action Buttons */}
@@ -213,7 +211,7 @@ export function NewDashboardPage() {
}}
>
<RefreshCw className="w-5 h-5" />
<span className="hidden sm:inline">Refresh</span>
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
</button>
{/* Unified Add Button */}
@@ -226,7 +224,7 @@ export function NewDashboardPage() {
}}
>
<Plus className="w-5 h-5" />
<span className="hidden sm:inline">Agregar</span>
<span className="hidden sm:inline">{t('common:actions.add')}</span>
<Sparkles className="w-4 h-4 opacity-80" />
</button>
</div>
@@ -272,20 +270,20 @@ export function NewDashboardPage() {
{/* SECTION 5: Quick Insights Grid */}
<div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Key Metrics</h2>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.key_metrics')}</h2>
<InsightsGrid insights={insights} loading={insightsLoading} />
</div>
{/* SECTION 6: Quick Action Links */}
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Quick Actions</h2>
<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
onClick={() => navigate('/app/operations/procurement')}
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-info)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>View Orders</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_orders')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-info)' }} />
</button>
@@ -294,7 +292,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-success)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Production</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_production')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-success)' }} />
</button>
@@ -303,7 +301,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-secondary)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Inventory</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_inventory')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-secondary)' }} />
</button>
@@ -312,7 +310,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-warning)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Suppliers</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_suppliers')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-warning)' }} />
</button>
</div>

View File

@@ -0,0 +1,377 @@
import React from 'react';
import { Bell, MessageSquare, Mail, AlertCircle, Globe } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { NotificationSettings } from '../../../../../api/types/settings';
import { useTranslation } from 'react-i18next';
interface NotificationSettingsCardProps {
settings: NotificationSettings;
onChange: (settings: NotificationSettings) => void;
disabled?: boolean;
}
const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const { t } = useTranslation('ajustes');
const handleChange = (field: keyof NotificationSettings) => (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const value = e.target.type === 'checkbox' ? (e.target as HTMLInputElement).checked :
e.target.value;
onChange({ ...settings, [field]: value });
};
const handleChannelChange = (field: 'po_notification_channels' | 'inventory_alert_channels' | 'production_alert_channels' | 'forecast_alert_channels', channel: string) => {
const currentChannels = settings[field];
const newChannels = currentChannels.includes(channel)
? currentChannels.filter(c => c !== channel)
: [...currentChannels, channel];
onChange({ ...settings, [field]: newChannels });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Bell className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
{t('notification.title')}
</h3>
<div className="space-y-6">
{/* WhatsApp Configuration */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<MessageSquare className="w-4 h-4 mr-2" />
{t('notification.whatsapp_config')}
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="whatsapp_enabled"
checked={settings.whatsapp_enabled}
onChange={handleChange('whatsapp_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="whatsapp_enabled" className="text-sm text-[var(--text-secondary)]">
{t('notification.whatsapp_enabled')}
</label>
</div>
{settings.whatsapp_enabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-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')}
/>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('notification.whatsapp_api_version')}
</label>
<select
value={settings.whatsapp_api_version}
onChange={handleChange('whatsapp_api_version')}
disabled={disabled}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
>
<option value="v18.0">v18.0</option>
<option value="v19.0">v19.0</option>
<option value="v20.0">v20.0</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('notification.whatsapp_default_language')}
</label>
<select
value={settings.whatsapp_default_language}
onChange={handleChange('whatsapp_default_language')}
disabled={disabled}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
>
<option value="es">Español</option>
<option value="eu">Euskara</option>
<option value="en">English</option>
</select>
</div>
</div>
)}
{settings.whatsapp_enabled && (
<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">
<AlertCircle 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>
</div>
</div>
</div>
)}
</div>
</div>
{/* Email Configuration */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Mail className="w-4 h-4 mr-2" />
{t('notification.email_config')}
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="email_enabled"
checked={settings.email_enabled}
onChange={handleChange('email_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="email_enabled" className="text-sm text-[var(--text-secondary)]">
{t('notification.email_enabled')}
</label>
</div>
{settings.email_enabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<Input
type="email"
label={t('notification.email_from_address')}
value={settings.email_from_address}
onChange={handleChange('email_from_address')}
disabled={disabled}
placeholder="orders@yourbakery.com"
/>
<Input
label={t('notification.email_from_name')}
value={settings.email_from_name}
onChange={handleChange('email_from_name')}
disabled={disabled}
placeholder="Your Bakery Name"
/>
<Input
type="email"
label={t('notification.email_reply_to')}
value={settings.email_reply_to}
onChange={handleChange('email_reply_to')}
disabled={disabled}
placeholder="info@yourbakery.com"
/>
</div>
)}
</div>
</div>
{/* Notification Preferences */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Globe className="w-4 h-4 mr-2" />
{t('notification.preferences')}
</h4>
<div className="space-y-4 pl-6">
{/* PO Notifications */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_po_notifications"
checked={settings.enable_po_notifications}
onChange={handleChange('enable_po_notifications')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_po_notifications" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_po_notifications')}
</label>
</div>
{settings.enable_po_notifications && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.po_notification_channels.includes('email')}
onChange={() => handleChannelChange('po_notification_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.po_notification_channels.includes('whatsapp')}
onChange={() => handleChannelChange('po_notification_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Inventory Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_inventory_alerts"
checked={settings.enable_inventory_alerts}
onChange={handleChange('enable_inventory_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_inventory_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_inventory_alerts')}
</label>
</div>
{settings.enable_inventory_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.inventory_alert_channels.includes('email')}
onChange={() => handleChannelChange('inventory_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.inventory_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('inventory_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Production Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_production_alerts"
checked={settings.enable_production_alerts}
onChange={handleChange('enable_production_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_production_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_production_alerts')}
</label>
</div>
{settings.enable_production_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.production_alert_channels.includes('email')}
onChange={() => handleChannelChange('production_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.production_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('production_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Forecast Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_forecast_alerts"
checked={settings.enable_forecast_alerts}
onChange={handleChange('enable_forecast_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_forecast_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_forecast_alerts')}
</label>
</div>
{settings.enable_forecast_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.forecast_alert_channels.includes('email')}
onChange={() => handleChannelChange('forecast_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.forecast_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('forecast_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
</div>
</div>
</div>
</Card>
);
};
export default NotificationSettingsCard;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, Loader } from 'lucide-react';
import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, Loader, Bell } from 'lucide-react';
import { Button, Card, Input, Select } from '../../../../components/ui';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
import { PageHeader } from '../../../../components/layout';
@@ -15,6 +15,7 @@ import type {
SupplierSettings,
POSSettings,
OrderSettings,
NotificationSettings,
} from '../../../../api/types/settings';
import ProcurementSettingsCard from '../../database/ajustes/cards/ProcurementSettingsCard';
import InventorySettingsCard from '../../database/ajustes/cards/InventorySettingsCard';
@@ -22,6 +23,7 @@ import ProductionSettingsCard from '../../database/ajustes/cards/ProductionSetti
import SupplierSettingsCard from '../../database/ajustes/cards/SupplierSettingsCard';
import POSSettingsCard from '../../database/ajustes/cards/POSSettingsCard';
import OrderSettingsCard from '../../database/ajustes/cards/OrderSettingsCard';
import NotificationSettingsCard from '../../database/ajustes/cards/NotificationSettingsCard';
interface BakeryConfig {
name: string;
@@ -98,6 +100,7 @@ const BakerySettingsPage: React.FC = () => {
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings | null>(null);
const [posSettings, setPosSettings] = useState<POSSettings | null>(null);
const [orderSettings, setOrderSettings] = useState<OrderSettings | null>(null);
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -137,6 +140,7 @@ const BakerySettingsPage: React.FC = () => {
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setNotificationSettings(settings.notification_settings);
}
}, [settings]);
@@ -232,7 +236,7 @@ const BakerySettingsPage: React.FC = () => {
const handleSaveOperationalSettings = async () => {
if (!tenantId || !procurementSettings || !inventorySettings || !productionSettings ||
!supplierSettings || !posSettings || !orderSettings) {
!supplierSettings || !posSettings || !orderSettings || !notificationSettings) {
return;
}
@@ -248,6 +252,7 @@ const BakerySettingsPage: React.FC = () => {
supplier_settings: supplierSettings,
pos_settings: posSettings,
order_settings: orderSettings,
notification_settings: notificationSettings,
},
});
@@ -314,6 +319,7 @@ const BakerySettingsPage: React.FC = () => {
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setNotificationSettings(settings.notification_settings);
}
setHasUnsavedChanges(false);
};
@@ -387,6 +393,10 @@ const BakerySettingsPage: React.FC = () => {
<SettingsIcon className="w-4 h-4 mr-2" />
{t('bakery.tabs.operations')}
</TabsTrigger>
<TabsTrigger value="notifications" className="flex-1 sm:flex-none whitespace-nowrap">
<Bell className="w-4 h-4 mr-2" />
{t('bakery.tabs.notifications')}
</TabsTrigger>
</TabsList>
{/* Tab 1: Information */}
@@ -689,6 +699,22 @@ const BakerySettingsPage: React.FC = () => {
)}
</div>
</TabsContent>
{/* Tab 4: Notifications */}
<TabsContent value="notifications">
<div className="space-y-6">
{notificationSettings && (
<NotificationSettingsCard
settings={notificationSettings}
onChange={(newSettings) => {
setNotificationSettings(newSettings);
handleOperationalSettingsChange();
}}
disabled={isLoading}
/>
)}
</div>
</TabsContent>
</Tabs>
{/* Floating Save Button */}
@@ -714,7 +740,7 @@ const BakerySettingsPage: React.FC = () => {
<Button
variant="primary"
size="sm"
onClick={activeTab === 'operations' ? handleSaveOperationalSettings : handleSaveConfig}
onClick={activeTab === 'operations' || activeTab === 'notifications' ? handleSaveOperationalSettings : handleSaveConfig}
isLoading={isLoading}
loadingText={t('common.saving')}
className="flex-1 sm:flex-none"