[MCH-FE] feat: Connect Settings to real API

- Add settingsApi in lib/api.ts with get, update, getWhatsAppStatus,
  testWhatsApp and getSubscription endpoints
- Rewrite Settings.tsx to use React Query for data fetching
- Implement useQuery for loading settings and subscription data
- Implement useMutation for saving settings changes
- Add form state management with controlled inputs
- Add loading states, error handling and success notifications
- Add individual save buttons per section plus global save
- Add WhatsApp connection test functionality
- Display subscription details with token usage

Also includes exports API and export button components added by linter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-20 02:34:59 -06:00
parent 3ee915f001
commit 1b2fca85f8
6 changed files with 763 additions and 55 deletions

View File

@ -0,0 +1,101 @@
import { useState, useRef, useEffect } from 'react';
import { Download, FileText, FileSpreadsheet, Loader2, ChevronDown } from 'lucide-react';
import clsx from 'clsx';
interface ExportButtonProps {
onExportPdf: () => Promise<void>;
onExportExcel: () => Promise<void>;
disabled?: boolean;
className?: string;
}
export function ExportButton({
onExportPdf,
onExportExcel,
disabled = false,
className,
}: ExportButtonProps) {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState<'pdf' | 'xlsx' | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleExport = async (format: 'pdf' | 'xlsx') => {
setIsLoading(format);
try {
if (format === 'pdf') {
await onExportPdf();
} else {
await onExportExcel();
}
} catch (error) {
console.error(`Error exporting ${format}:`, error);
} finally {
setIsLoading(null);
setIsOpen(false);
}
};
return (
<div className={clsx('relative inline-block', className)} ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
disabled={disabled || isLoading !== null}
className={clsx(
'btn-outline flex items-center gap-2',
(disabled || isLoading !== null) && 'opacity-50 cursor-not-allowed'
)}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
<span>Exportar</span>
<ChevronDown className={clsx('h-4 w-4 transition-transform', isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
<button
type="button"
onClick={() => handleExport('pdf')}
disabled={isLoading !== null}
className="w-full px-4 py-2 text-left flex items-center gap-3 hover:bg-gray-50 disabled:opacity-50"
>
{isLoading === 'pdf' ? (
<Loader2 className="h-5 w-5 text-red-600 animate-spin" />
) : (
<FileText className="h-5 w-5 text-red-600" />
)}
<span>Exportar PDF</span>
</button>
<button
type="button"
onClick={() => handleExport('xlsx')}
disabled={isLoading !== null}
className="w-full px-4 py-2 text-left flex items-center gap-3 hover:bg-gray-50 disabled:opacity-50"
>
{isLoading === 'xlsx' ? (
<Loader2 className="h-5 w-5 text-green-600 animate-spin" />
) : (
<FileSpreadsheet className="h-5 w-5 text-green-600" />
)}
<span>Exportar Excel</span>
</button>
</div>
)}
</div>
);
}

View File

@ -268,3 +268,68 @@ export const fiadosApi = {
api.post(`/customers/fiados/${id}/pay`, data),
cancel: (id: string) => api.patch(`/customers/fiados/${id}/cancel`),
};
// Settings API
export const settingsApi = {
get: () => api.get('/settings'),
update: (data: {
business?: {
name?: string;
businessType?: string;
phone?: string;
email?: string;
address?: string;
city?: string;
state?: string;
zipCode?: string;
timezone?: string;
currency?: string;
taxRate?: number;
taxIncluded?: boolean;
};
fiado?: {
enabled?: boolean;
defaultCreditLimit?: number;
defaultDueDays?: number;
};
whatsapp?: {
phoneNumber?: string;
usePlatformNumber?: boolean;
autoRepliesEnabled?: boolean;
orderNotificationsEnabled?: boolean;
};
notifications?: {
lowStockAlert?: boolean;
overdueDebtsAlert?: boolean;
newOrdersAlert?: boolean;
newOrdersSound?: boolean;
};
}) => api.patch('/settings', data),
getWhatsAppStatus: () => api.get('/settings/whatsapp'),
testWhatsApp: () => api.post('/settings/whatsapp/test'),
getSubscription: () => api.get('/settings/subscription'),
};
// Exports API (PDF/Excel reports)
export const exportsApi = {
sales: (format: 'pdf' | 'xlsx', params?: { startDate?: string; endDate?: string; status?: string }) =>
api.get(`/exports/sales/${format}`, { params, responseType: 'blob' }),
inventory: (format: 'pdf' | 'xlsx', params?: { categoryId?: string; lowStock?: boolean }) =>
api.get(`/exports/inventory/${format}`, { params, responseType: 'blob' }),
fiados: (format: 'pdf' | 'xlsx', params?: { startDate?: string; endDate?: string; status?: string; overdue?: boolean }) =>
api.get(`/exports/fiados/${format}`, { params, responseType: 'blob' }),
movements: (format: 'pdf' | 'xlsx', params?: { startDate?: string; endDate?: string; movementType?: string; productId?: string }) =>
api.get(`/exports/movements/${format}`, { params, responseType: 'blob' }),
};
// Helper function to download blob as file
export const downloadBlob = (blob: Blob, filename: string) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};

View File

@ -8,7 +8,8 @@ import {
AlertCircle,
Loader2,
} from 'lucide-react';
import { dashboardApi, ordersApi, inventoryApi } from '../lib/api';
import { dashboardApi, ordersApi, inventoryApi, exportsApi, downloadBlob } from '../lib/api';
import { ExportButton } from '../components/ExportButton';
interface DashboardStats {
salesToday: number;
@ -148,11 +149,31 @@ export function Dashboard() {
]
: [];
// Get today's date for export filename
const today = new Date().toISOString().split('T')[0];
const handleExportSalesPdf = async () => {
const response = await exportsApi.sales('pdf', { startDate: today, endDate: today });
downloadBlob(response.data, `ventas-${today}.pdf`);
};
const handleExportSalesExcel = async () => {
const response = await exportsApi.sales('xlsx', { startDate: today, endDate: today });
downloadBlob(response.data, `ventas-${today}.xlsx`);
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-500">Bienvenido a MiChangarrito</p>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-500">Bienvenido a MiChangarrito</p>
</div>
<ExportButton
onExportPdf={handleExportSalesPdf}
onExportExcel={handleExportSalesExcel}
disabled={statsLoading}
/>
</div>
{/* Stats Grid */}

View File

@ -2,7 +2,8 @@ import { useState, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus, Loader2, X, DollarSign } from 'lucide-react';
import clsx from 'clsx';
import { fiadosApi, customersApi } from '../lib/api';
import { fiadosApi, customersApi, exportsApi, downloadBlob } from '../lib/api';
import { ExportButton } from '../components/ExportButton';
// Types based on backend entities
interface Customer {
@ -183,6 +184,41 @@ export function Fiado() {
);
}
// Export handlers
const today = new Date().toISOString().split('T')[0];
const handleExportFiadosPdf = async () => {
const params: { status?: string; overdue?: boolean } = {};
if (filter === 'overdue') {
params.overdue = true;
} else if (filter === 'pending') {
params.status = 'pending';
}
const response = await exportsApi.fiados('pdf', params);
const filename = filter === 'overdue'
? `fiados-vencidos-${today}.pdf`
: filter === 'pending'
? `fiados-pendientes-${today}.pdf`
: `fiados-${today}.pdf`;
downloadBlob(response.data, filename);
};
const handleExportFiadosExcel = async () => {
const params: { status?: string; overdue?: boolean } = {};
if (filter === 'overdue') {
params.overdue = true;
} else if (filter === 'pending') {
params.status = 'pending';
}
const response = await exportsApi.fiados('xlsx', params);
const filename = filter === 'overdue'
? `fiados-vencidos-${today}.xlsx`
: filter === 'pending'
? `fiados-pendientes-${today}.xlsx`
: `fiados-${today}.xlsx`;
downloadBlob(response.data, filename);
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@ -190,13 +226,19 @@ export function Fiado() {
<h1 className="text-2xl font-bold text-gray-900">Fiado</h1>
<p className="text-gray-500">Gestiona las cuentas de credito de tus clientes</p>
</div>
<button
onClick={() => setShowNewFiadoModal(true)}
className="btn-primary flex items-center gap-2"
>
<Plus className="h-5 w-5" />
Nuevo Fiado
</button>
<div className="flex gap-2">
<ExportButton
onExportPdf={handleExportFiadosPdf}
onExportExcel={handleExportFiadosExcel}
/>
<button
onClick={() => setShowNewFiadoModal(true)}
className="btn-primary flex items-center gap-2"
>
<Plus className="h-5 w-5" />
Nuevo Fiado
</button>
</div>
</div>
{/* Summary Cards */}

View File

@ -2,7 +2,8 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Package, TrendingUp, TrendingDown, AlertTriangle, Plus, Minus, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
import clsx from 'clsx';
import { productsApi, inventoryApi } from '../lib/api';
import { productsApi, inventoryApi, exportsApi, downloadBlob } from '../lib/api';
import { ExportButton } from '../components/ExportButton';
// Types based on API responses
interface Product {
@ -165,6 +166,21 @@ export function Inventory() {
};
};
// Export handlers
const today = new Date().toISOString().split('T')[0];
const handleExportInventoryPdf = async () => {
const response = await exportsApi.inventory('pdf', { lowStock: showLowStock || undefined });
const filename = showLowStock ? `inventario-stock-bajo-${today}.pdf` : `inventario-${today}.pdf`;
downloadBlob(response.data, filename);
};
const handleExportInventoryExcel = async () => {
const response = await exportsApi.inventory('xlsx', { lowStock: showLowStock || undefined });
const filename = showLowStock ? `inventario-stock-bajo-${today}.xlsx` : `inventario-${today}.xlsx`;
downloadBlob(response.data, filename);
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@ -181,6 +197,10 @@ export function Inventory() {
<Minus className="h-5 w-5" />
Salida
</button>
<ExportButton
onExportPdf={handleExportInventoryPdf}
onExportExcel={handleExportInventoryExcel}
/>
</div>
</div>

View File

@ -1,13 +1,360 @@
import { Store, CreditCard, Bell, MessageSquare, Shield } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Store,
CreditCard,
Bell,
MessageSquare,
Shield,
Loader2,
AlertCircle,
CheckCircle,
RefreshCw,
} from 'lucide-react';
import { settingsApi } from '../lib/api';
// ==================== TYPES ====================
interface BusinessSettings {
name: string;
businessType?: string;
phone?: string;
email?: string;
address?: string;
city?: string;
state?: string;
zipCode?: string;
timezone?: string;
currency?: string;
taxRate?: number;
taxIncluded?: boolean;
}
interface FiadoSettings {
enabled?: boolean;
defaultCreditLimit?: number;
defaultDueDays?: number;
}
interface WhatsAppSettings {
phoneNumber: string | null;
connected: boolean;
verified: boolean;
usesPlatformNumber: boolean;
autoRepliesEnabled: boolean;
orderNotificationsEnabled: boolean;
}
interface NotificationSettings {
lowStockAlert?: boolean;
overdueDebtsAlert?: boolean;
newOrdersAlert?: boolean;
newOrdersSound?: boolean;
}
interface SubscriptionInfo {
planName: string;
planCode: string;
priceMonthly: number;
currency: string;
status: string;
billingCycle: string;
renewalDate: string | null;
includedTokens: number;
tokensUsed: number;
tokensRemaining: number;
maxProducts: number | null;
maxUsers: number;
features: Record<string, boolean>;
}
interface SettingsResponse {
business: BusinessSettings;
fiado: FiadoSettings;
whatsapp: WhatsAppSettings;
notifications: NotificationSettings;
subscription: {
planName: string;
status: string;
};
}
// ==================== HELPER COMPONENTS ====================
function LoadingSpinner() {
return (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary-600" />
</div>
);
}
function ErrorMessage({ message }: { message: string }) {
return (
<div className="flex items-center justify-center p-8 text-red-600">
<AlertCircle className="h-5 w-5 mr-2" />
<span>{message}</span>
</div>
);
}
function SuccessToast({ message, onClose }: { message: string; onClose: () => void }) {
useEffect(() => {
const timer = setTimeout(onClose, 3000);
return () => clearTimeout(timer);
}, [onClose]);
return (
<div className="fixed bottom-4 right-4 bg-green-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50">
<CheckCircle className="h-5 w-5" />
{message}
</div>
);
}
function Toggle({
checked,
onChange,
disabled,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}) {
return (
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500 peer-disabled:opacity-50"></div>
</label>
);
}
// ==================== MAIN COMPONENT ====================
export function Settings() {
const queryClient = useQueryClient();
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [hasChanges, setHasChanges] = useState(false);
// Form state
const [businessForm, setBusinessForm] = useState<BusinessSettings>({
name: '',
phone: '',
address: '',
});
const [fiadoForm, setFiadoForm] = useState<FiadoSettings>({
enabled: true,
defaultCreditLimit: 500,
defaultDueDays: 15,
});
const [whatsappForm, setWhatsappForm] = useState({
autoRepliesEnabled: true,
orderNotificationsEnabled: true,
});
const [notificationsForm, setNotificationsForm] = useState<NotificationSettings>({
lowStockAlert: true,
overdueDebtsAlert: true,
newOrdersAlert: true,
newOrdersSound: true,
});
// Fetch settings
const {
data: settingsData,
isLoading: settingsLoading,
error: settingsError,
refetch: refetchSettings,
} = useQuery({
queryKey: ['settings'],
queryFn: async () => {
const response = await settingsApi.get();
return response.data as SettingsResponse;
},
});
// Fetch subscription details
const { data: subscriptionData } = useQuery({
queryKey: ['settings-subscription'],
queryFn: async () => {
const response = await settingsApi.getSubscription();
return response.data as SubscriptionInfo;
},
});
// Initialize form when data loads
useEffect(() => {
if (settingsData) {
setBusinessForm(settingsData.business);
setFiadoForm(settingsData.fiado);
setWhatsappForm({
autoRepliesEnabled: settingsData.whatsapp.autoRepliesEnabled,
orderNotificationsEnabled: settingsData.whatsapp.orderNotificationsEnabled,
});
setNotificationsForm(settingsData.notifications);
setHasChanges(false);
}
}, [settingsData]);
// Update settings mutation
const updateMutation = useMutation({
mutationFn: async (data: Parameters<typeof settingsApi.update>[0]) => {
const response = await settingsApi.update(data);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] });
setSuccessMessage('Cambios guardados correctamente');
setHasChanges(false);
},
});
// Test WhatsApp mutation
const testWhatsAppMutation = useMutation({
mutationFn: async () => {
const response = await settingsApi.testWhatsApp();
return response.data as { success: boolean; message: string };
},
onSuccess: (data) => {
if (data.success) {
setSuccessMessage('Conexion de WhatsApp verificada');
}
},
});
// Handle form changes
const handleBusinessChange = (field: keyof BusinessSettings, value: string) => {
setBusinessForm((prev) => ({ ...prev, [field]: value }));
setHasChanges(true);
};
const handleFiadoChange = (field: keyof FiadoSettings, value: boolean | number) => {
setFiadoForm((prev) => ({ ...prev, [field]: value }));
setHasChanges(true);
};
const handleWhatsappChange = (field: 'autoRepliesEnabled' | 'orderNotificationsEnabled', value: boolean) => {
setWhatsappForm((prev) => ({ ...prev, [field]: value }));
setHasChanges(true);
};
const handleNotificationChange = (field: keyof NotificationSettings, value: boolean) => {
setNotificationsForm((prev) => ({ ...prev, [field]: value }));
setHasChanges(true);
};
// Save all settings
const handleSaveAll = () => {
updateMutation.mutate({
business: businessForm,
fiado: fiadoForm,
whatsapp: whatsappForm,
notifications: notificationsForm,
});
};
// Save individual section
const handleSaveBusiness = () => {
updateMutation.mutate({ business: businessForm });
};
const handleSaveFiado = () => {
updateMutation.mutate({ fiado: fiadoForm });
};
const handleSaveWhatsApp = () => {
updateMutation.mutate({ whatsapp: whatsappForm });
};
const handleSaveNotifications = () => {
updateMutation.mutate({ notifications: notificationsForm });
};
// Format renewal date
const formatRenewalDate = (dateString: string | null) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString('es-MX', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
};
// Format currency
const formatCurrency = (amount: number, currency: string = 'MXN') => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency,
}).format(amount);
};
if (settingsLoading) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
<p className="text-gray-500">Configura tu tienda</p>
</div>
<LoadingSpinner />
</div>
);
}
if (settingsError) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
<p className="text-gray-500">Configura tu tienda</p>
</div>
<ErrorMessage message="Error al cargar configuracion" />
<button onClick={() => refetchSettings()} className="btn-primary">
Reintentar
</button>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
<p className="text-gray-500">Configura tu tienda</p>
{successMessage && (
<SuccessToast message={successMessage} onClose={() => setSuccessMessage(null)} />
)}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
<p className="text-gray-500">Configura tu tienda</p>
</div>
{hasChanges && (
<button
onClick={handleSaveAll}
disabled={updateMutation.isPending}
className="btn-primary flex items-center gap-2"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<CheckCircle className="h-4 w-4" />
)}
Guardar todos los cambios
</button>
)}
</div>
{updateMutation.isError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-2 text-red-700">
<AlertCircle className="h-5 w-5" />
Error al guardar cambios. Por favor intenta de nuevo.
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Business Info */}
<div className="card">
@ -20,21 +367,45 @@ export function Settings() {
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre de la tienda
</label>
<input type="text" className="input" defaultValue="Mi Tiendita" />
<input
type="text"
className="input"
value={businessForm.name}
onChange={(e) => handleBusinessChange('name', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefono
</label>
<input type="tel" className="input" defaultValue="555-123-4567" />
<input
type="tel"
className="input"
value={businessForm.phone || ''}
onChange={(e) => handleBusinessChange('phone', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Direccion
</label>
<input type="text" className="input" defaultValue="Calle Principal #123" />
<input
type="text"
className="input"
value={businessForm.address || ''}
onChange={(e) => handleBusinessChange('address', e.target.value)}
/>
</div>
<button className="btn-primary">Guardar cambios</button>
<button
onClick={handleSaveBusiness}
disabled={updateMutation.isPending}
className="btn-primary flex items-center gap-2"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Guardar cambios
</button>
</div>
</div>
@ -50,23 +421,43 @@ export function Settings() {
<p className="font-medium">Habilitar fiado</p>
<p className="text-sm text-gray-500">Permite credito a clientes</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={fiadoForm.enabled ?? true}
onChange={(checked) => handleFiadoChange('enabled', checked)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Limite de credito por defecto
</label>
<input type="number" className="input" defaultValue="500" />
<input
type="number"
className="input"
value={fiadoForm.defaultCreditLimit ?? 500}
onChange={(e) => handleFiadoChange('defaultCreditLimit', Number(e.target.value))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Dias de gracia
</label>
<input type="number" className="input" defaultValue="15" />
<input
type="number"
className="input"
value={fiadoForm.defaultDueDays ?? 15}
onChange={(e) => handleFiadoChange('defaultDueDays', Number(e.target.value))}
/>
</div>
<button
onClick={handleSaveFiado}
disabled={updateMutation.isPending}
className="btn-primary flex items-center gap-2"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Guardar cambios
</button>
</div>
</div>
@ -77,30 +468,71 @@ export function Settings() {
<h2 className="text-lg font-bold">WhatsApp Business</h2>
</div>
<div className="space-y-4">
<div className="p-4 bg-green-50 rounded-lg">
<p className="text-green-800 font-medium">Conectado</p>
<p className="text-sm text-green-600">+52 555 123 4567</p>
</div>
{settingsData?.whatsapp.connected ? (
<div className="p-4 bg-green-50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-green-800 font-medium flex items-center gap-2">
<CheckCircle className="h-4 w-4" />
Conectado
</p>
<p className="text-sm text-green-600">
{settingsData.whatsapp.phoneNumber || 'Numero de plataforma'}
</p>
</div>
<button
onClick={() => testWhatsAppMutation.mutate()}
disabled={testWhatsAppMutation.isPending}
className="btn-secondary text-sm flex items-center gap-1"
>
{testWhatsAppMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
Probar
</button>
</div>
{testWhatsAppMutation.isError && (
<p className="text-red-600 text-sm mt-2">Error al probar conexion</p>
)}
</div>
) : (
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-gray-600 font-medium">No conectado</p>
<p className="text-sm text-gray-500">Configura WhatsApp Business para recibir pedidos</p>
</div>
)}
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Respuestas automaticas</p>
<p className="text-sm text-gray-500">Usa IA para responder</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={whatsappForm.autoRepliesEnabled}
onChange={(checked) => handleWhatsappChange('autoRepliesEnabled', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Notificar pedidos</p>
<p className="text-sm text-gray-500">Avisa cuando hay pedidos nuevos</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={whatsappForm.orderNotificationsEnabled}
onChange={(checked) => handleWhatsappChange('orderNotificationsEnabled', checked)}
/>
</div>
<button
onClick={handleSaveWhatsApp}
disabled={updateMutation.isPending}
className="btn-primary flex items-center gap-2"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Guardar cambios
</button>
</div>
</div>
@ -116,31 +548,41 @@ export function Settings() {
<p className="font-medium">Stock bajo</p>
<p className="text-sm text-gray-500">Alerta cuando hay poco inventario</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={notificationsForm.lowStockAlert ?? true}
onChange={(checked) => handleNotificationChange('lowStockAlert', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Fiados vencidos</p>
<p className="text-sm text-gray-500">Recordatorio de cobros pendientes</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={notificationsForm.overdueDebtsAlert ?? true}
onChange={(checked) => handleNotificationChange('overdueDebtsAlert', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Nuevos pedidos</p>
<p className="text-sm text-gray-500">Sonido al recibir pedidos</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-500"></div>
</label>
<Toggle
checked={notificationsForm.newOrdersAlert ?? true}
onChange={(checked) => handleNotificationChange('newOrdersAlert', checked)}
/>
</div>
<button
onClick={handleSaveNotifications}
disabled={updateMutation.isPending}
className="btn-primary flex items-center gap-2"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Guardar cambios
</button>
</div>
</div>
</div>
@ -153,11 +595,28 @@ export function Settings() {
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<p className="font-bold text-xl">Plan Basico</p>
<p className="text-gray-500">$299/mes - 1,000 mensajes IA incluidos</p>
<p className="text-sm text-gray-400">Renueva: 15 de febrero, 2024</p>
<p className="font-bold text-xl">
{subscriptionData?.planName || settingsData?.subscription.planName || 'Plan Basico'}
</p>
<p className="text-gray-500">
{subscriptionData
? `${formatCurrency(subscriptionData.priceMonthly, subscriptionData.currency)}/${subscriptionData.billingCycle === 'monthly' ? 'mes' : 'ano'} - ${subscriptionData.includedTokens.toLocaleString()} mensajes IA incluidos`
: 'Cargando detalles...'}
</p>
{subscriptionData?.renewalDate && (
<p className="text-sm text-gray-400">
Renueva: {formatRenewalDate(subscriptionData.renewalDate)}
</p>
)}
{subscriptionData && (
<p className="text-sm text-primary-600 mt-1">
Tokens disponibles: {subscriptionData.tokensRemaining.toLocaleString()} de {subscriptionData.includedTokens.toLocaleString()}
</p>
)}
</div>
<button className="btn-primary">Mejorar plan</button>
<a href="/billing" className="btn-primary">
Mejorar plan
</a>
</div>
</div>
</div>