feat: recordatorios periódicos; supervisor invita auxiliares; owner edita usuarios; precios planes y MSI

This commit is contained in:
Horux Dev
2026-06-25 20:56:39 +00:00
parent 0bded50e57
commit 57c4908e68
12 changed files with 770 additions and 102 deletions

View File

@@ -2,7 +2,7 @@
import { useState } from 'react';
import { DashboardShell } from '@/components/layouts/dashboard-shell';
import { Card, CardContent, CardHeader, CardTitle, Button, Input, Label } from '@horux/shared-ui';
import { Card, CardContent, CardHeader, CardTitle, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@horux/shared-ui';
import { useEventos, useCreateEvento, useUpdateEvento, useDeleteEvento } from '@/lib/hooks/use-calendario';
import { useAuthStore } from '@/stores/auth-store';
import {
@@ -42,6 +42,8 @@ interface RecordatorioForm {
fechaLimite: string;
notas: string;
privado: boolean;
recurrencia: 'unica' | 'mensual' | 'bimestral' | 'trimestral' | 'anual';
fechaFin: string;
}
const emptyForm: RecordatorioForm = {
@@ -50,6 +52,8 @@ const emptyForm: RecordatorioForm = {
fechaLimite: '',
notas: '',
privado: false,
recurrencia: 'unica',
fechaFin: '',
};
export default function CalendarioPage() {
@@ -100,6 +104,8 @@ export default function CalendarioPage() {
fechaLimite: evento.fechaLimite,
notas: evento.notas || '',
privado: (evento as any).privado ?? false,
recurrencia: (evento.recurrencia as RecordatorioForm['recurrencia']) || 'unica',
fechaFin: '', // La fecha fin no se edita desde el calendario; se mantiene la original
});
setShowForm(true);
};
@@ -113,15 +119,19 @@ export default function CalendarioPage() {
data: { titulo: form.titulo, descripcion: form.descripcion, fechaLimite: form.fechaLimite, notas: form.notas, privado: form.privado } as any,
});
} else {
await createEvento.mutateAsync({
const payload: any = {
titulo: form.titulo,
descripcion: form.descripcion,
tipo: 'custom',
fechaLimite: form.fechaLimite,
recurrencia: 'unica',
recurrencia: form.recurrencia,
notas: form.notas,
privado: form.privado,
} as any);
};
if (form.recurrencia !== 'unica' && form.fechaFin) {
payload.fechaFin = form.fechaFin;
}
await createEvento.mutateAsync(payload);
}
setShowForm(false);
setForm(emptyForm);
@@ -131,10 +141,15 @@ export default function CalendarioPage() {
}
};
const handleDelete = async (id: number) => {
if (!confirm('¿Eliminar este recordatorio?')) return;
const handleDelete = async (evento: EventoFiscal) => {
if (!evento.id) return;
const esPeriodico = evento.recurrencia && evento.recurrencia !== 'unica';
const mensaje = esPeriodico
? 'Esto cancelará todas las ocurrencias futuras de esta serie. ¿Continuar?'
: '¿Eliminar este recordatorio?';
if (!confirm(mensaje)) return;
try {
await deleteEvento.mutateAsync(id);
await deleteEvento.mutateAsync(evento.id);
} catch {
alert('Error al eliminar');
}
@@ -206,6 +221,44 @@ export default function CalendarioPage() {
/>
</div>
</div>
{!editingId && (
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="recurrencia">Recurrencia</Label>
<Select
value={form.recurrencia}
onValueChange={(v) => setForm({ ...form, recurrencia: v as RecordatorioForm['recurrencia'] })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unica">Única</SelectItem>
<SelectItem value="mensual">Mensual</SelectItem>
<SelectItem value="bimestral">Bimestral</SelectItem>
<SelectItem value="trimestral">Trimestral</SelectItem>
<SelectItem value="anual">Anual</SelectItem>
</SelectContent>
</Select>
</div>
{form.recurrencia !== 'unica' && (
<div className="space-y-2">
<Label htmlFor="fechaFin">Fecha fin (opcional)</Label>
<Input
id="fechaFin"
type="date"
value={form.fechaFin}
onChange={e => setForm({ ...form, fechaFin: e.target.value })}
/>
</div>
)}
</div>
)}
{editingId && form.recurrencia !== 'unica' && (
<div className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
Este es un recordatorio periódico. Los cambios se aplicarán a todas las ocurrencias futuras.
</div>
)}
<div className="space-y-2">
<Label htmlFor="descripcion">Descripción (opcional)</Label>
<Input
@@ -417,7 +470,7 @@ export default function CalendarioPage() {
</Button>
<Button
variant="ghost" size="icon" className="h-7 w-7 text-destructive"
onClick={() => evento.id && handleDelete(evento.id)}
onClick={() => handleDelete(evento)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>