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 { AdminService } from '../services/admin.service';
|
||||||
|
import { AdsV2Service } from '../services/ads.v2.service';
|
||||||
import ModerationModal from '../components/ModerationModal';
|
import ModerationModal from '../components/ModerationModal';
|
||||||
import UserModal from '../components/UserModal';
|
import UserModal from '../components/UserModal';
|
||||||
import { parseUTCDate, getImageUrl } from '../utils/app.utils';
|
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 AdDetailsModal from '../components/AdDetailsModal';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import ConfirmationModal from '../components/ConfirmationModal';
|
||||||
|
|
||||||
type TabType = 'stats' | 'ads' | 'moderation' | 'transactions' | 'users' | 'audit';
|
type TabType = 'stats' | 'ads' | 'moderation' | 'transactions' | 'users' | 'audit';
|
||||||
|
|
||||||
@@ -21,6 +23,69 @@ export default function AdminPage() {
|
|||||||
const [selectedAd, setSelectedAd] = useState<any>(null);
|
const [selectedAd, setSelectedAd] = useState<any>(null);
|
||||||
const [selectedUser, setSelectedUser] = useState<number | null>(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
|
// Estados para filtros de Usuarios
|
||||||
const [userSearch, setUserSearch] = useState('');
|
const [userSearch, setUserSearch] = useState('');
|
||||||
const [userPage, setUserPage] = useState(1);
|
const [userPage, setUserPage] = useState(1);
|
||||||
@@ -292,7 +357,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lista de Avisos (Escritorio / Tabla) */}
|
{/* 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">
|
<table className="w-full text-left">
|
||||||
<thead className="bg-white/5">
|
<thead className="bg-white/5">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -303,10 +368,9 @@ export default function AdminPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/5">
|
<tbody className="divide-y divide-white/5">
|
||||||
{data.ads.map((ad: any) => {
|
{data.ads.map((ad: any, index: number) => {
|
||||||
const statusConfig = STATUS_CONFIG[ad.statusID] || { label: 'Desc.', bg: 'bg-gray-500', color: 'text-white' };
|
|
||||||
return (
|
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">
|
<td className="px-8 py-5">
|
||||||
<div className="flex items-center gap-4">
|
<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="" />
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-8 py-5">
|
<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}`}>
|
<div className="w-40 relative">
|
||||||
{statusConfig.label}
|
<StatusDropdown
|
||||||
</span>
|
currentStatus={ad.statusID}
|
||||||
|
onChange={(newStatus) => initiateStatusChange(ad.adID, newStatus)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-8 py-5 text-right">
|
<td className="px-8 py-5 text-right">
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
@@ -367,11 +434,10 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
{/* Lista de Avisos (Móvil / Cards) */}
|
{/* Lista de Avisos (Móvil / Cards) */}
|
||||||
<div className="md:hidden space-y-4">
|
<div className="md:hidden space-y-4">
|
||||||
{data.ads.map((ad: any) => {
|
{data.ads.map((ad: any, index: number) => {
|
||||||
const statusConfig = STATUS_CONFIG[ad.statusID] || { label: 'Desc.', bg: 'bg-gray-500', color: 'text-white' };
|
|
||||||
return (
|
return (
|
||||||
<div key={ad.adID} className="glass p-5 rounded-3xl border border-white/5 space-y-4 shadow-xl">
|
<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">
|
<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="" />
|
<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-1 min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||||
@@ -382,9 +448,12 @@ export default function AdminPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}`}>
|
<div className="mt-2 w-full max-w-[200px]">
|
||||||
{statusConfig.label}
|
<StatusDropdown
|
||||||
</span>
|
currentStatus={ad.statusID}
|
||||||
|
onChange={(newStatus) => initiateStatusChange(ad.adID, newStatus)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -965,6 +1034,21 @@ export default function AdminPage() {
|
|||||||
onUpdate={loadData}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -986,4 +1070,87 @@ function DashboardMiniCard({ label, value, icon, color = 'blue' }: { label: stri
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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