- Backend: agrega 'supervisor' a authorize() de rutas: - POST/DELETE /contribuyentes/:id/fiel - POST /contribuyentes/:id/facturapi/csd - POST/DELETE /contribuyentes/:id/obligaciones/* - Frontend: muestra tarjeta 'Obligaciones Fiscales' en /configuracion para rol supervisor
648 lines
26 KiB
TypeScript
648 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { Header } from '@/components/layouts/header';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Label, Input } from '@horux/shared-ui';
|
|
import { useThemeStore } from '@/stores/theme-store';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { themes, type ThemeName } from '@/themes';
|
|
import { Check, Palette, User, Building, Sidebar, PanelTop, Minimize2, Sparkles, RefreshCw, Scale, Trash2, MapPin, KeyRound, Tags, Receipt, Bell, Package } from 'lucide-react';
|
|
import { isGlobalAdminRfc, isDespachoTenant } from '@horux/shared';
|
|
import Link from 'next/link';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import { useBancos, useCreateBanco, useDeleteBanco } from '@/lib/hooks/use-bancos';
|
|
import { useTenantViewStore } from '@/stores/tenant-view-store';
|
|
import { useContribuyenteStore } from '@/stores/contribuyente-store';
|
|
|
|
const themeOptions: { name: ThemeName; label: string; description: string; layoutDesc: string; layoutIcon: typeof Sidebar }[] = [
|
|
{
|
|
name: 'light',
|
|
label: 'Light',
|
|
description: 'Tema claro profesional',
|
|
layoutDesc: 'Sidebar estándar fijo',
|
|
layoutIcon: Sidebar,
|
|
},
|
|
{
|
|
name: 'dark',
|
|
label: 'Dark',
|
|
description: 'Modo oscuro con acentos neón',
|
|
layoutDesc: 'Sidebar flotante con efecto glass',
|
|
layoutIcon: Sparkles,
|
|
},
|
|
];
|
|
|
|
function RegimenesActivosSection() {
|
|
const queryClient = useQueryClient();
|
|
const [saving, setSaving] = useState(false);
|
|
const { viewingTenantId } = useTenantViewStore();
|
|
const { selectedContribuyenteId } = useContribuyenteStore();
|
|
const user = useAuthStore(s => s.user);
|
|
const isDespacho = isDespachoTenant(user?.tenantRfc);
|
|
|
|
const tenantKey = viewingTenantId || 'own';
|
|
|
|
const { data: catalogo } = useQuery({
|
|
queryKey: ['regimenes-catalogo'],
|
|
queryFn: async () => {
|
|
const res = await apiClient.get<{ id: number; clave: string; descripcion: string; tipoPersona: string }[]>('/regimenes');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
// Despacho: read régimen from contribuyente; Horux360: from tenant activos
|
|
const { data: activos } = useQuery({
|
|
queryKey: ['regimenes-activos', tenantKey, selectedContribuyenteId],
|
|
queryFn: async () => {
|
|
if (isDespacho && selectedContribuyenteId) {
|
|
const res = await apiClient.get(`/contribuyentes/${selectedContribuyenteId}`);
|
|
const c = res.data;
|
|
// Build activos array from regimen_fiscal field (may be comma-separated: "623,606,612")
|
|
if (c.regimenFiscal && catalogo) {
|
|
const claves = c.regimenFiscal.split(',').map((s: string) => s.trim());
|
|
return claves
|
|
.map((clave: string) => catalogo.find((cat: any) => cat.clave === clave))
|
|
.filter(Boolean)
|
|
.map((cat: any) => ({ id: cat.id, clave: cat.clave, descripcion: cat.descripcion }));
|
|
}
|
|
return [];
|
|
}
|
|
const res = await apiClient.get<{ id: number; clave: string; descripcion: string }[]>('/regimenes/activos');
|
|
return res.data;
|
|
},
|
|
enabled: !!catalogo,
|
|
});
|
|
|
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
|
|
|
useEffect(() => {
|
|
if (activos && catalogo) {
|
|
const ids = new Set(activos.map((a: { clave: string }) => catalogo.find((c: { clave: string; id: number }) => c.clave === a.clave)?.id).filter(Boolean) as number[]);
|
|
setSelected(ids);
|
|
}
|
|
}, [activos, catalogo]);
|
|
|
|
const toggle = (id: number) => {
|
|
setSelected(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
try {
|
|
await apiClient.put('/regimenes/activos', { regimenIds: Array.from(selected) });
|
|
queryClient.invalidateQueries({ queryKey: ['regimenes-activos', tenantKey, selectedContribuyenteId] });
|
|
queryClient.invalidateQueries({ queryKey: ['calendario'] });
|
|
queryClient.invalidateQueries({ queryKey: ['regimenes-periodo'] });
|
|
} catch {
|
|
alert('Error al guardar');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (!catalogo) return null;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Scale className="h-4 w-4" />
|
|
Regimenes Fiscales Activos
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Selecciona los regimenes fiscales bajo los que opera tu empresa. Esto afecta el calendario de obligaciones y los filtros disponibles.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-2 md:grid-cols-2">
|
|
{catalogo.map(r => (
|
|
<button
|
|
key={r.id}
|
|
onClick={() => toggle(r.id)}
|
|
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
|
|
selected.has(r.id)
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border hover:border-primary/30'
|
|
}`}
|
|
>
|
|
<div className={`h-5 w-5 rounded border-2 flex items-center justify-center flex-shrink-0 ${
|
|
selected.has(r.id) ? 'border-primary bg-primary' : 'border-muted-foreground/30'
|
|
}`}>
|
|
{selected.has(r.id) && <Check className="h-3 w-3 text-primary-foreground" />}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<span className="text-xs font-mono font-bold text-muted-foreground">{r.clave}</span>
|
|
<p className="text-sm truncate">{r.descripcion}</p>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<p className="text-xs text-muted-foreground">{selected.size} regimenes seleccionados</p>
|
|
<Button onClick={handleSave} disabled={saving} size="sm">
|
|
{saving ? 'Guardando...' : 'Guardar'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function DomicilioFiscalSection() {
|
|
const queryClient = useQueryClient();
|
|
const { viewingTenantId } = useTenantViewStore();
|
|
const { selectedContribuyenteId } = useContribuyenteStore();
|
|
const user = useAuthStore(s => s.user);
|
|
const isDespacho = isDespachoTenant(user?.tenantRfc);
|
|
const tenantKey = viewingTenantId || 'own';
|
|
const [saving, setSaving] = useState(false);
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|
|
|
// Despacho: read domicilio from contribuyente; Horux360: read from tenant datos-fiscales
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['datos-fiscales', tenantKey, selectedContribuyenteId],
|
|
queryFn: async () => {
|
|
if (isDespacho && selectedContribuyenteId) {
|
|
const res = await apiClient.get(`/contribuyentes/${selectedContribuyenteId}`);
|
|
const c = res.data;
|
|
const dom = c.domicilio || {};
|
|
return {
|
|
codigoPostal: c.codigoPostal || dom.codigoPostal || '',
|
|
calle: dom.calle || '',
|
|
numExterior: dom.numExterior || '',
|
|
numInterior: dom.numInterior || '',
|
|
colonia: dom.colonia || '',
|
|
ciudad: dom.ciudad || '',
|
|
municipio: dom.municipio || '',
|
|
estado: dom.estado || '',
|
|
telefono: dom.telefono || '',
|
|
};
|
|
}
|
|
const res = await apiClient.get('/facturacion/datos-fiscales');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
const [form, setForm] = useState({
|
|
codigoPostal: '', calle: '', numExterior: '', numInterior: '',
|
|
colonia: '', ciudad: '', municipio: '', estado: '', telefono: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
setForm({
|
|
codigoPostal: data.codigoPostal || '',
|
|
calle: data.calle || '',
|
|
numExterior: data.numExterior || '',
|
|
numInterior: data.numInterior || '',
|
|
colonia: data.colonia || '',
|
|
ciudad: data.ciudad || '',
|
|
municipio: data.municipio || '',
|
|
estado: data.estado || '',
|
|
telefono: data.telefono || '',
|
|
});
|
|
}
|
|
}, [data]);
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
setMsg(null);
|
|
try {
|
|
if (isDespacho && selectedContribuyenteId) {
|
|
// Save domicilio to contribuyente
|
|
await apiClient.put(`/contribuyentes/${selectedContribuyenteId}`, {
|
|
domicilio: form,
|
|
codigoPostal: form.codigoPostal,
|
|
});
|
|
} else {
|
|
await apiClient.put('/facturacion/datos-fiscales', form);
|
|
}
|
|
queryClient.invalidateQueries({ queryKey: ['datos-fiscales', tenantKey, selectedContribuyenteId] });
|
|
setMsg('Datos guardados');
|
|
setTimeout(() => setMsg(null), 3000);
|
|
} catch {
|
|
setMsg('Error al guardar');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) return null;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<MapPin className="h-4 w-4" />
|
|
Domicilio Fiscal
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Dirección y teléfono de la empresa. Se usa para facturas al público en general.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div className="space-y-2">
|
|
<Label>Código Postal</Label>
|
|
<Input value={form.codigoPostal} onChange={e => setForm({ ...form, codigoPostal: e.target.value.replace(/\D/g, '').slice(0, 5) })} placeholder="06600" maxLength={5} />
|
|
</div>
|
|
<div className="space-y-2 md:col-span-2">
|
|
<Label>Calle</Label>
|
|
<Input value={form.calle} onChange={e => setForm({ ...form, calle: e.target.value })} placeholder="Av. Reforma" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Num. Exterior</Label>
|
|
<Input value={form.numExterior} onChange={e => setForm({ ...form, numExterior: e.target.value })} placeholder="123" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Num. Interior</Label>
|
|
<Input value={form.numInterior} onChange={e => setForm({ ...form, numInterior: e.target.value })} placeholder="4A" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Colonia</Label>
|
|
<Input value={form.colonia} onChange={e => setForm({ ...form, colonia: e.target.value })} placeholder="Juárez" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Ciudad</Label>
|
|
<Input value={form.ciudad} onChange={e => setForm({ ...form, ciudad: e.target.value })} placeholder="Ciudad de México" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Municipio</Label>
|
|
<Input value={form.municipio} onChange={e => setForm({ ...form, municipio: e.target.value })} placeholder="Cuauhtémoc" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Estado</Label>
|
|
<Input value={form.estado} onChange={e => setForm({ ...form, estado: e.target.value })} placeholder="CDMX" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Teléfono / Celular</Label>
|
|
<Input value={form.telefono} onChange={e => setForm({ ...form, telefono: e.target.value.replace(/[^\d+\-() ]/g, '').slice(0, 20) })} placeholder="+52 55 1234 5678" />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
{msg && <p className={`text-sm ${msg.includes('Error') ? 'text-red-600' : 'text-green-600'}`}>{msg}</p>}
|
|
{!msg && <div />}
|
|
<Button onClick={handleSave} disabled={saving} size="sm">
|
|
{saving ? 'Guardando...' : 'Guardar'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function BancosSection() {
|
|
const { data: bancos, isLoading } = useBancos();
|
|
const createBanco = useCreateBanco();
|
|
const deleteBancoMut = useDeleteBanco();
|
|
const [nombre, setNombre] = useState('');
|
|
const [terminacion, setTerminacion] = useState('');
|
|
|
|
const handleAdd = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!nombre || !terminacion) return;
|
|
try {
|
|
await createBanco.mutateAsync({ banco: nombre, terminacionCuenta: terminacion });
|
|
setNombre('');
|
|
setTerminacion('');
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || 'Error al crear banco');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: number) => {
|
|
if (!confirm('Eliminar este banco?')) return;
|
|
try {
|
|
await deleteBancoMut.mutateAsync(id);
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || 'Error al eliminar');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Building className="h-5 w-5" />
|
|
Bancos
|
|
</CardTitle>
|
|
<CardDescription>Cuentas bancarias para conciliacion</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{isLoading ? (
|
|
<p className="text-sm text-muted-foreground">Cargando...</p>
|
|
) : bancos && bancos.length > 0 ? (
|
|
<div className="divide-y">
|
|
{bancos.map((b) => (
|
|
<div key={b.id} className="flex items-center justify-between py-2">
|
|
<div>
|
|
<span className="font-medium">{b.banco}</span>
|
|
<span className="text-muted-foreground ml-2">****{b.terminacionCuenta}</span>
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDelete(b.id)}>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No hay bancos registrados</p>
|
|
)}
|
|
|
|
<form onSubmit={handleAdd} className="flex gap-2 items-end">
|
|
<div className="flex-1 space-y-1">
|
|
<Label htmlFor="banco-nombre">Banco</Label>
|
|
<Input id="banco-nombre" value={nombre} onChange={e => setNombre(e.target.value)} placeholder="BBVA" required />
|
|
</div>
|
|
<div className="w-32 space-y-1">
|
|
<Label htmlFor="banco-term">Terminacion</Label>
|
|
<Input id="banco-term" value={terminacion} onChange={e => setTerminacion(e.target.value.replace(/\D/g, '').slice(0, 4))} placeholder="1234" maxLength={4} required />
|
|
</div>
|
|
<Button type="submit" disabled={createBanco.isPending}>Agregar</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function ConfiguracionPage() {
|
|
const { theme, setTheme } = useThemeStore();
|
|
const { user } = useAuthStore();
|
|
const { viewingTenantName } = useTenantViewStore();
|
|
const { selectedContribuyenteId, selectedContribuyenteRfc, selectedContribuyenteNombre } = useContribuyenteStore();
|
|
|
|
const empresaNombre = viewingTenantName || user?.tenantName;
|
|
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
|
|
const isDespacho = isDespachoTenant(user?.tenantRfc);
|
|
|
|
return (
|
|
<>
|
|
<Header title="Configuración" />
|
|
<main className="p-6 space-y-6">
|
|
{/* User Info */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<User className="h-4 w-4" />
|
|
Información del Usuario
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Nombre</p>
|
|
<p className="font-medium">{user?.nombre}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Email</p>
|
|
<p className="font-medium">{user?.email}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Rol</p>
|
|
<p className="font-medium capitalize">{user?.role}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Company Info */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Building className="h-4 w-4" />
|
|
Información de la Empresa
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Empresa</p>
|
|
<p className="font-medium">{empresaNombre}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Contribuyente header — shown when despacho has one selected */}
|
|
{isDespacho && selectedContribuyenteId && (
|
|
<Card className="bg-primary/5 border-primary/20">
|
|
<CardContent className="py-3 px-5 flex items-center gap-2">
|
|
<Building className="h-4 w-4 text-primary" />
|
|
<span className="text-sm font-medium">Configuración de: {selectedContribuyenteNombre}</span>
|
|
<span className="text-xs text-muted-foreground font-mono">{selectedContribuyenteRfc}</span>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Regímenes Fiscales, Domicilio Fiscal, Bancos */}
|
|
{(user?.role === 'owner' || user?.role === 'cfo') && (
|
|
isDespacho && !selectedContribuyenteId ? (
|
|
<Card>
|
|
<CardContent className="py-6 text-center text-muted-foreground">
|
|
<p>Selecciona un contribuyente en el header para ver su configuración fiscal.</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<RegimenesActivosSection />
|
|
<DomicilioFiscalSection />
|
|
<BancosSection />
|
|
</>
|
|
)
|
|
)}
|
|
|
|
{/* SAT Configuration */}
|
|
<Link href="/configuracion/sat">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<RefreshCw className="h-4 w-4" />
|
|
Sincronizacion SAT
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Configura tu FIEL y la sincronizacion automatica de CFDIs con el SAT
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Descarga automaticamente tus facturas emitidas y recibidas directamente del portal del SAT.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
{/* Obligaciones Fiscales */}
|
|
{(user?.role === 'owner' || user?.role === 'cfo' || user?.role === 'supervisor') && (
|
|
<Link href="/configuracion/obligaciones">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Tags className="h-4 w-4" />
|
|
Obligaciones Fiscales
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Gestiona las obligaciones fiscales de tus contribuyentes
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Recibe recomendaciones basadas en el régimen fiscal, agrega o elimina obligaciones según las necesidades de cada RFC.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
)}
|
|
|
|
{/* Notificaciones */}
|
|
<Link href="/configuracion/notificaciones">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Bell className="h-4 w-4" />
|
|
Notificaciones
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Activa o desactiva los correos informativos por contribuyente
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Controla qué correos quieres recibir por cada cliente: documentos subidos, reporte semanal, recordatorios fiscales, vencimiento de suscripción.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
{/* Preferencias de Facturación (auto-emisión de pagos de suscripción) */}
|
|
<Link href="/configuracion/facturacion">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Receipt className="h-4 w-4" />
|
|
Preferencias de Facturación
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Define cómo facturamos los pagos de tu suscripción a Horux 360
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Elige si tus facturas salen con tus datos fiscales o como Público en General. Configura el uso CFDI (G03 Gastos en general / S01 Sin obligaciones) y el régimen a usar si tienes varios activos.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
{/* Seguridad */}
|
|
<Link href="/configuracion/seguridad">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<KeyRound className="h-4 w-4" />
|
|
Seguridad
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Cambia tu contraseña y gestiona las sesiones activas de tu cuenta
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Actualiza tu contraseña o cierra todas las sesiones activas si sospechas un acceso no autorizado.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
{/* CSD / Facturapi */}
|
|
<Link href="/configuracion/csd">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Building className="h-4 w-4" />
|
|
Certificado de Sello Digital (CSD)
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Configura tu CSD para emitir facturas electrónicas desde Horux360
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Sube tu certificado y llave privada para timbrar CFDIs directamente desde la plataforma.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
{/* Admin global: edición de precios */}
|
|
{isGlobalAdmin && (
|
|
<>
|
|
<Link href="/configuracion/precios-suscripcion">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Tags className="h-4 w-4" />
|
|
Precios de suscripciones
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Modifica los precios de los planes Starter, Business, Business + IA y Enterprise.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Los cambios aplican a suscripciones nuevas y renovaciones. Las vigentes conservan el precio contratado.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href="/configuracion/precios-timbres">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Receipt className="h-4 w-4" />
|
|
Precios de timbres adicionales
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Modifica los precios de los paquetes de timbres adicionales (100, 1000, 10000).
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Los cambios aplican a compras nuevas. Los paquetes ya vendidos conservan el precio que pagó el cliente.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href="/configuracion/addons">
|
|
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Package className="h-4 w-4" />
|
|
Add-ons del catálogo
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Gestiona los complementos disponibles (Lolita IA, RFCs extra, módulo IA, etc.).
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Modifica nombre, precio y disponibilidad de cada add-on. Los add-ons ya contratados
|
|
conservan el precio al que se cobraron; los cambios aplican a contrataciones nuevas.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
</main>
|
|
</>
|
|
);
|
|
}
|