Feat: Cambio de Estados en Menu Admin - Avisos

This commit is contained in:
2026-02-21 18:21:37 -03:00
parent 2c3b7b2336
commit 18a142e070

View File

@@ -1,11 +1,13 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { AdminService } from '../services/admin.service';
import { AdsV2Service } from '../services/ads.v2.service';
import ModerationModal from '../components/ModerationModal';
import UserModal from '../components/UserModal';
import { parseUTCDate, getImageUrl } from '../utils/app.utils';
import { STATUS_CONFIG } from '../constants/adStatuses';
import { STATUS_CONFIG, AD_STATUSES } from '../constants/adStatuses';
import AdDetailsModal from '../components/AdDetailsModal';
import { Link } from 'react-router-dom';
import ConfirmationModal from '../components/ConfirmationModal';
type TabType = 'stats' | 'ads' | 'moderation' | 'transactions' | 'users' | 'audit';
@@ -21,6 +23,69 @@ export default function AdminPage() {
const [selectedAd, setSelectedAd] = useState<any>(null);
const [selectedUser, setSelectedUser] = useState<number | null>(null);
const [modalConfig, setModalConfig] = useState<{
isOpen: boolean;
title: string;
message: string;
adId: number | null;
newStatus: number | null;
isDanger: boolean;
}>({
isOpen: false,
title: '',
message: '',
adId: null,
newStatus: null,
isDanger: false
});
const initiateStatusChange = (adId: number, newStatus: number) => {
let title = "Cambiar Estado";
let message = "¿Estás seguro de realizar esta acción?";
let isDanger = false;
if (newStatus === AD_STATUSES.DELETED) {
title = "¿Eliminar Aviso?";
message = "Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?";
isDanger = true;
} else if (newStatus === AD_STATUSES.PAUSED) {
title = "Pausar Publicación";
message = "Al pausar el aviso dejará de ser visible en los listados.\n\nPodrás reactivarlo cuando quieras.";
} else if (newStatus === AD_STATUSES.SOLD) {
title = "¡Felicitaciones!";
message = "Al marcar como VENDIDO el aviso mostrará la etiqueta \"Vendido\" al público.\n\n¿Confirmas que ya vendiste el vehículo?";
} else if (newStatus === AD_STATUSES.RESERVED) {
title = "Reservar Vehículo";
message = "Se indicará a los interesados que el vehículo está reservado.\n\n¿Deseas continuar?";
} else if (newStatus === AD_STATUSES.ACTIVE) {
title = "Reactivar Aviso";
message = "El aviso volverá a estar visible para todos y recibirás consultas nuevamente.";
}
setModalConfig({
isOpen: true,
title,
message,
adId,
newStatus,
isDanger,
});
};
// Acción real al confirmar en el modal
const confirmStatusChange = async () => {
const { adId, newStatus } = modalConfig;
if (!adId || !newStatus) return;
try {
setModalConfig({ ...modalConfig, isOpen: false });
await AdsV2Service.changeStatus(adId, newStatus);
loadData();
} catch (error) {
alert("Error al actualizar estado");
}
};
// Estados para filtros de Usuarios
const [userSearch, setUserSearch] = useState('');
const [userPage, setUserPage] = useState(1);
@@ -292,7 +357,7 @@ export default function AdminPage() {
</div>
{/* Lista de Avisos (Escritorio / Tabla) */}
<div className="hidden md:block glass rounded-[2.5rem] overflow-hidden border border-white/5">
<div className="hidden md:block glass rounded-[2.5rem] overflow-visible border border-white/5">
<table className="w-full text-left">
<thead className="bg-white/5">
<tr>
@@ -303,10 +368,9 @@ export default function AdminPage() {
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{data.ads.map((ad: any) => {
const statusConfig = STATUS_CONFIG[ad.statusID] || { label: 'Desc.', bg: 'bg-gray-500', color: 'text-white' };
{data.ads.map((ad: any, index: number) => {
return (
<tr key={ad.adID} className="hover:bg-white/5 transition-colors">
<tr key={ad.adID} className="hover:bg-white/5 transition-colors relative" style={{ zIndex: 50 - index }}>
<td className="px-8 py-5">
<div className="flex items-center gap-4">
<img src={getImageUrl(ad.thumbnail)} className="w-20 h-14 object-cover rounded-xl border border-white/10" alt="" />
@@ -330,9 +394,12 @@ export default function AdminPage() {
</div>
</td>
<td className="px-8 py-5">
<span className={`px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-tighter border ${statusConfig.bg} ${statusConfig.color} ${statusConfig.border}`}>
{statusConfig.label}
</span>
<div className="w-40 relative">
<StatusDropdown
currentStatus={ad.statusID}
onChange={(newStatus) => initiateStatusChange(ad.adID, newStatus)}
/>
</div>
</td>
<td className="px-8 py-5 text-right">
<div className="flex flex-col items-end gap-2">
@@ -367,11 +434,10 @@ export default function AdminPage() {
{/* Lista de Avisos (Móvil / Cards) */}
<div className="md:hidden space-y-4">
{data.ads.map((ad: any) => {
const statusConfig = STATUS_CONFIG[ad.statusID] || { label: 'Desc.', bg: 'bg-gray-500', color: 'text-white' };
{data.ads.map((ad: any, index: number) => {
return (
<div key={ad.adID} className="glass p-5 rounded-3xl border border-white/5 space-y-4 shadow-xl">
<div className="flex gap-4">
<div key={ad.adID} className="glass p-5 rounded-3xl border border-white/5 space-y-4 shadow-xl relative" style={{ zIndex: 50 - index }}>
<div className="flex gap-4 items-start">
<img src={getImageUrl(ad.thumbnail)} className="w-24 h-16 object-cover rounded-xl border border-white/10" alt="" />
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
@@ -382,9 +448,12 @@ export default function AdminPage() {
</span>
)}
</div>
<span className={`inline-block px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-tighter border ${statusConfig.bg} ${statusConfig.color} ${statusConfig.border}`}>
{statusConfig.label}
</span>
<div className="mt-2 w-full max-w-[200px]">
<StatusDropdown
currentStatus={ad.statusID}
onChange={(newStatus) => initiateStatusChange(ad.adID, newStatus)}
/>
</div>
</div>
</div>
@@ -965,6 +1034,21 @@ export default function AdminPage() {
onUpdate={loadData}
/>
)}
{/* MODAL DE CONFIRMACIÓN */}
<ConfirmationModal
isOpen={modalConfig.isOpen}
title={modalConfig.title}
message={modalConfig.message}
onConfirm={confirmStatusChange}
onCancel={() => setModalConfig({ ...modalConfig, isOpen: false })}
isDanger={modalConfig.isDanger}
confirmText={
modalConfig.newStatus === AD_STATUSES.SOLD
? '¡Sí, vendido!'
: 'Confirmar'
}
/>
</div>
);
}
@@ -987,3 +1071,86 @@ function DashboardMiniCard({ label, value, icon, color = 'blue' }: { label: stri
</div>
);
}
// DROPDOWN DE ESTADO
function StatusDropdown({
currentStatus,
onChange,
}: {
currentStatus: number;
onChange: (val: number) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
// Fallback seguro si currentStatus no tiene config
const currentConfig = STATUS_CONFIG[currentStatus] || {
label: "Desconocido",
color: "text-gray-400",
bg: "bg-gray-500/10",
border: "border-gray-500/20",
icon: "❓",
};
const ALLOWED_STATUSES = [
AD_STATUSES.ACTIVE,
AD_STATUSES.PAUSED,
AD_STATUSES.RESERVED,
AD_STATUSES.SOLD,
AD_STATUSES.DELETED,
];
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative w-full" ref={wrapperRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl border ${currentConfig.bg} ${currentConfig.border} ${currentConfig.color} transition-all hover:brightness-110`}
>
<div className="flex items-center gap-2">
<span>{currentConfig.icon}</span>
<span className="text-[10px] font-black uppercase tracking-widest">
{currentConfig.label}
</span>
</div>
<span className="text-xs"></span>
</button>
{isOpen && (
<div className="absolute z-[100] w-full mt-2 bg-[#1a1d24] border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-fade-in ring-1 ring-white/5 left-0">
{ALLOWED_STATUSES.map((statusId) => {
const config = STATUS_CONFIG[statusId];
if (!config) return null;
return (
<button
key={statusId}
onClick={() => {
onChange(statusId);
setIsOpen(false);
}}
className={`w-full text-left px-4 py-3 text-[10px] font-bold uppercase tracking-widest hover:bg-white/5 transition-colors border-b border-white/5 last:border-0 flex items-center gap-2 ${statusId === currentStatus ? "text-white bg-white/5" : "text-gray-400"}`}
>
<span className="text-sm">{config.icon}</span>
{config.label}
</button>
);
})}
</div>
)}
</div>
);
}