[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),
|
api.post(`/customers/fiados/${id}/pay`, data),
|
||||||
cancel: (id: string) => api.patch(`/customers/fiados/${id}/cancel`),
|
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,
|
AlertCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} 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 {
|
interface DashboardStats {
|
||||||
salesToday: number;
|
salesToday: number;
|
||||||
@ -148,12 +149,32 @@ 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||||
<p className="text-gray-500">Bienvenido a MiChangarrito</p>
|
<p className="text-gray-500">Bienvenido a MiChangarrito</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ExportButton
|
||||||
|
onExportPdf={handleExportSalesPdf}
|
||||||
|
onExportExcel={handleExportSalesExcel}
|
||||||
|
disabled={statsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useState, useMemo } from 'react';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus, Loader2, X, DollarSign } from 'lucide-react';
|
import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus, Loader2, X, DollarSign } from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
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
|
// Types based on backend entities
|
||||||
interface Customer {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
@ -190,6 +226,11 @@ export function Fiado() {
|
|||||||
<h1 className="text-2xl font-bold text-gray-900">Fiado</h1>
|
<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>
|
<p className="text-gray-500">Gestiona las cuentas de credito de tus clientes</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<ExportButton
|
||||||
|
onExportPdf={handleExportFiadosPdf}
|
||||||
|
onExportExcel={handleExportFiadosExcel}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowNewFiadoModal(true)}
|
onClick={() => setShowNewFiadoModal(true)}
|
||||||
className="btn-primary flex items-center gap-2"
|
className="btn-primary flex items-center gap-2"
|
||||||
@ -198,6 +239,7 @@ export function Fiado() {
|
|||||||
Nuevo Fiado
|
Nuevo Fiado
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useState } from 'react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Package, TrendingUp, TrendingDown, AlertTriangle, Plus, Minus, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
import { Package, TrendingUp, TrendingDown, AlertTriangle, Plus, Minus, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
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
|
// Types based on API responses
|
||||||
interface Product {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<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" />
|
<Minus className="h-5 w-5" />
|
||||||
Salida
|
Salida
|
||||||
</button>
|
</button>
|
||||||
|
<ExportButton
|
||||||
|
onExportPdf={handleExportInventoryPdf}
|
||||||
|
onExportExcel={handleExportInventoryExcel}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,359 @@
|
|||||||
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() {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Ajustes</h1>
|
||||||
<p className="text-gray-500">Configura tu tienda</p>
|
<p className="text-gray-500">Configura tu tienda</p>
|
||||||
</div>
|
</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">
|
||||||
|
{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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Business Info */}
|
{/* Business Info */}
|
||||||
@ -20,21 +367,45 @@ export function Settings() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Nombre de la tienda
|
Nombre de la tienda
|
||||||
</label>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Telefono
|
Telefono
|
||||||
</label>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Direccion
|
Direccion
|
||||||
</label>
|
</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -50,23 +421,43 @@ export function Settings() {
|
|||||||
<p className="font-medium">Habilitar fiado</p>
|
<p className="font-medium">Habilitar fiado</p>
|
||||||
<p className="text-sm text-gray-500">Permite credito a clientes</p>
|
<p className="text-sm text-gray-500">Permite credito a clientes</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={fiadoForm.enabled ?? true}
|
||||||
<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>
|
onChange={(checked) => handleFiadoChange('enabled', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Limite de credito por defecto
|
Limite de credito por defecto
|
||||||
</label>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Dias de gracia
|
Dias de gracia
|
||||||
</label>
|
</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -77,30 +468,71 @@ export function Settings() {
|
|||||||
<h2 className="text-lg font-bold">WhatsApp Business</h2>
|
<h2 className="text-lg font-bold">WhatsApp Business</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{settingsData?.whatsapp.connected ? (
|
||||||
<div className="p-4 bg-green-50 rounded-lg">
|
<div className="p-4 bg-green-50 rounded-lg">
|
||||||
<p className="text-green-800 font-medium">Conectado</p>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-green-600">+52 555 123 4567</p>
|
<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>
|
</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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Respuestas automaticas</p>
|
<p className="font-medium">Respuestas automaticas</p>
|
||||||
<p className="text-sm text-gray-500">Usa IA para responder</p>
|
<p className="text-sm text-gray-500">Usa IA para responder</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={whatsappForm.autoRepliesEnabled}
|
||||||
<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>
|
onChange={(checked) => handleWhatsappChange('autoRepliesEnabled', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Notificar pedidos</p>
|
<p className="font-medium">Notificar pedidos</p>
|
||||||
<p className="text-sm text-gray-500">Avisa cuando hay pedidos nuevos</p>
|
<p className="text-sm text-gray-500">Avisa cuando hay pedidos nuevos</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={whatsappForm.orderNotificationsEnabled}
|
||||||
<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>
|
onChange={(checked) => handleWhatsappChange('orderNotificationsEnabled', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,31 +548,41 @@ export function Settings() {
|
|||||||
<p className="font-medium">Stock bajo</p>
|
<p className="font-medium">Stock bajo</p>
|
||||||
<p className="text-sm text-gray-500">Alerta cuando hay poco inventario</p>
|
<p className="text-sm text-gray-500">Alerta cuando hay poco inventario</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={notificationsForm.lowStockAlert ?? true}
|
||||||
<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>
|
onChange={(checked) => handleNotificationChange('lowStockAlert', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Fiados vencidos</p>
|
<p className="font-medium">Fiados vencidos</p>
|
||||||
<p className="text-sm text-gray-500">Recordatorio de cobros pendientes</p>
|
<p className="text-sm text-gray-500">Recordatorio de cobros pendientes</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={notificationsForm.overdueDebtsAlert ?? true}
|
||||||
<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>
|
onChange={(checked) => handleNotificationChange('overdueDebtsAlert', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Nuevos pedidos</p>
|
<p className="font-medium">Nuevos pedidos</p>
|
||||||
<p className="text-sm text-gray-500">Sonido al recibir pedidos</p>
|
<p className="text-sm text-gray-500">Sonido al recibir pedidos</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<Toggle
|
||||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
checked={notificationsForm.newOrdersAlert ?? true}
|
||||||
<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>
|
onChange={(checked) => handleNotificationChange('newOrdersAlert', checked)}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -153,11 +595,28 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-xl">Plan Basico</p>
|
<p className="font-bold text-xl">
|
||||||
<p className="text-gray-500">$299/mes - 1,000 mensajes IA incluidos</p>
|
{subscriptionData?.planName || settingsData?.subscription.planName || 'Plan Basico'}
|
||||||
<p className="text-sm text-gray-400">Renueva: 15 de febrero, 2024</p>
|
</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>
|
</div>
|
||||||
<button className="btn-primary">Mejorar plan</button>
|
<a href="/billing" className="btn-primary">
|
||||||
|
Mejorar plan
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user