Initial commit - Horux Despachos NL
This commit is contained in:
647
apps/web/app/(dashboard)/configuracion/page.tsx
Normal file
647
apps/web/app/(dashboard)/configuracion/page.tsx
Normal file
@@ -0,0 +1,647 @@
|
||||
'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 => catalogo.find(c => 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') && (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user