Feat: Cambio de Estados en Menu Admin - Avisos
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user