From 1b2fca85f8279bebd6004b781ab5d3a9b48a4c9b Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Tue, 20 Jan 2026 02:34:59 -0600 Subject: [PATCH] [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 --- src/components/ExportButton.tsx | 101 ++++++ src/lib/api.ts | 65 ++++ src/pages/Dashboard.tsx | 29 +- src/pages/Fiado.tsx | 58 +++- src/pages/Inventory.tsx | 22 +- src/pages/Settings.tsx | 543 +++++++++++++++++++++++++++++--- 6 files changed, 763 insertions(+), 55 deletions(-) create mode 100644 src/components/ExportButton.tsx diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx new file mode 100644 index 0000000..449bdf5 --- /dev/null +++ b/src/components/ExportButton.tsx @@ -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; + onExportExcel: () => Promise; + 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(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 ( +
+ + + {isOpen && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 481532b..8cf38d0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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); +}; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 3ddbb07..c429afc 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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 (
-
-

Dashboard

-

Bienvenido a MiChangarrito

+
+
+

Dashboard

+

Bienvenido a MiChangarrito

+
+
{/* Stats Grid */} diff --git a/src/pages/Fiado.tsx b/src/pages/Fiado.tsx index e9890a1..09b672b 100644 --- a/src/pages/Fiado.tsx +++ b/src/pages/Fiado.tsx @@ -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 (
@@ -190,13 +226,19 @@ export function Fiado() {

Fiado

Gestiona las cuentas de credito de tus clientes

- +
+ + +
{/* Summary Cards */} diff --git a/src/pages/Inventory.tsx b/src/pages/Inventory.tsx index c67b8e0..4a95caf 100644 --- a/src/pages/Inventory.tsx +++ b/src/pages/Inventory.tsx @@ -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 (
@@ -181,6 +197,10 @@ export function Inventory() { Salida +
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index d0b6cde..ddffee1 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -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; +} + +interface SettingsResponse { + business: BusinessSettings; + fiado: FiadoSettings; + whatsapp: WhatsAppSettings; + notifications: NotificationSettings; + subscription: { + planName: string; + status: string; + }; +} + +// ==================== HELPER COMPONENTS ==================== + +function LoadingSpinner() { + return ( +
+ +
+ ); +} + +function ErrorMessage({ message }: { message: string }) { + return ( +
+ + {message} +
+ ); +} + +function SuccessToast({ message, onClose }: { message: string; onClose: () => void }) { + useEffect(() => { + const timer = setTimeout(onClose, 3000); + return () => clearTimeout(timer); + }, [onClose]); + + return ( +
+ + {message} +
+ ); +} + +function Toggle({ + checked, + onChange, + disabled, +}: { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; +}) { + return ( + + ); +} + +// ==================== MAIN COMPONENT ==================== export function Settings() { + const queryClient = useQueryClient(); + const [successMessage, setSuccessMessage] = useState(null); + const [hasChanges, setHasChanges] = useState(false); + + // Form state + const [businessForm, setBusinessForm] = useState({ + name: '', + phone: '', + address: '', + }); + const [fiadoForm, setFiadoForm] = useState({ + enabled: true, + defaultCreditLimit: 500, + defaultDueDays: 15, + }); + const [whatsappForm, setWhatsappForm] = useState({ + autoRepliesEnabled: true, + orderNotificationsEnabled: true, + }); + const [notificationsForm, setNotificationsForm] = useState({ + 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[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 ( +
+
+

Ajustes

+

Configura tu tienda

+
+ +
+ ); + } + + if (settingsError) { + return ( +
+
+

Ajustes

+

Configura tu tienda

+
+ + +
+ ); + } + return (
-
-

Ajustes

-

Configura tu tienda

+ {successMessage && ( + setSuccessMessage(null)} /> + )} + +
+
+

Ajustes

+

Configura tu tienda

+
+ {hasChanges && ( + + )}
+ {updateMutation.isError && ( +
+ + Error al guardar cambios. Por favor intenta de nuevo. +
+ )} +
{/* Business Info */}
@@ -20,21 +367,45 @@ export function Settings() { - + handleBusinessChange('name', e.target.value)} + />
- + handleBusinessChange('phone', e.target.value)} + />
- + handleBusinessChange('address', e.target.value)} + />
- +
@@ -50,23 +421,43 @@ export function Settings() {

Habilitar fiado

Permite credito a clientes

- + handleFiadoChange('enabled', checked)} + />
- + handleFiadoChange('defaultCreditLimit', Number(e.target.value))} + />
- + handleFiadoChange('defaultDueDays', Number(e.target.value))} + />
+
@@ -77,30 +468,71 @@ export function Settings() {

WhatsApp Business

-
-

Conectado

-

+52 555 123 4567

-
+ {settingsData?.whatsapp.connected ? ( +
+
+
+

+ + Conectado +

+

+ {settingsData.whatsapp.phoneNumber || 'Numero de plataforma'} +

+
+ +
+ {testWhatsAppMutation.isError && ( +

Error al probar conexion

+ )} +
+ ) : ( +
+

No conectado

+

Configura WhatsApp Business para recibir pedidos

+
+ )}

Respuestas automaticas

Usa IA para responder

- + handleWhatsappChange('autoRepliesEnabled', checked)} + />

Notificar pedidos

Avisa cuando hay pedidos nuevos

- + handleWhatsappChange('orderNotificationsEnabled', checked)} + />
+
@@ -116,31 +548,41 @@ export function Settings() {

Stock bajo

Alerta cuando hay poco inventario

- + handleNotificationChange('lowStockAlert', checked)} + />

Fiados vencidos

Recordatorio de cobros pendientes

- + handleNotificationChange('overdueDebtsAlert', checked)} + />

Nuevos pedidos

Sonido al recibir pedidos

- + handleNotificationChange('newOrdersAlert', checked)} + />
+ @@ -153,11 +595,28 @@ export function Settings() {
-

Plan Basico

-

$299/mes - 1,000 mensajes IA incluidos

-

Renueva: 15 de febrero, 2024

+

+ {subscriptionData?.planName || settingsData?.subscription.planName || 'Plan Basico'} +

+

+ {subscriptionData + ? `${formatCurrency(subscriptionData.priceMonthly, subscriptionData.currency)}/${subscriptionData.billingCycle === 'monthly' ? 'mes' : 'ano'} - ${subscriptionData.includedTokens.toLocaleString()} mensajes IA incluidos` + : 'Cargando detalles...'} +

+ {subscriptionData?.renewalDate && ( +

+ Renueva: {formatRenewalDate(subscriptionData.renewalDate)} +

+ )} + {subscriptionData && ( +

+ Tokens disponibles: {subscriptionData.tokensRemaining.toLocaleString()} de {subscriptionData.includedTokens.toLocaleString()} +

+ )}
- + + Mejorar plan +