[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:
parent
3ee915f001
commit
1b2fca85f8
101
src/components/ExportButton.tsx
Normal file
101
src/components/ExportButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user