const Admin = () => {
const t = window.t || (k => k);
const {toast, confirm} = useApp();
const [tab, setTab] = useState('overview');
const [tickets, setTickets] = useState([]);
const [replyText, setReplyText] = useState({});
const [supportFilter, setSupportFilter] = useState('open');
const [expandedTk, setExpandedTk] = useState({});
const [users, setUsers] = useState([]);
const [metrics, setMetrics] = useState(null);
const [logs, setLogs] = useState([]);
const [logLevel, setLogLevel] = useState('');
const [search, setSearch] = useState('');
const [selectedUser, setSelectedUser] = useState(null);
const [userDetail, setUserDetail] = useState(null);
const [detailTab, setDetailTab] = useState('activity');
const [loadingDetail, setLoadingDetail] = useState(false);
const [expandedSub, setExpandedSub] = useState(null);
const [subViewMode, setSubViewMode] = useState({}); // {subId: 'summary'|'post'}
const [giftMonths, setGiftMonths] = useState(1);
const [showGiftFor, setShowGiftFor] = useState(null);
const [showCreateUser, setShowCreateUser] = useState(false);
const [newUser, setNewUser] = useState({email:'',password:'',name:'',license:'PPL',plan:'free'});
const [showChangePass, setShowChangePass] = useState(null);
const [newPass, setNewPass] = useState('');
const [ifrRoutes, setIfrRoutes] = useState(() => JSON.parse(document.getElementById('ifr-data')?.textContent || '[]'));
const [editingRoute, setEditingRoute] = useState(null);
const [routeForm, setRouteForm] = useState({from:'',to:'',rules:'Z',fl:'',route:'',dur:'',notes:''});
const [userFilter, setUserFilter] = useState('all'); // all|premium|gifted|free|blocked|instructor
// Load data from API
useEffect(() => {
api.adminMetrics().then(d => setMetrics(d)).catch(() => {});
api.adminUsers('').then(d => setUsers(d.users || [])).catch(() => {});
}, []);
const logsLoaded = useRef('');
const supportLoaded = useRef(false);
useEffect(() => {
if (tab === 'logs' && logsLoaded.current !== logLevel) {
logsLoaded.current = logLevel;
api.adminLogs(logLevel).then(d => setLogs(d.logs || [])).catch(() => {});
}
}, [tab, logLevel]);
useEffect(() => {
if (tab === 'support' && !supportLoaded.current) {
supportLoaded.current = true;
api.adminSupport().then(d => setTickets(d.tickets || [])).catch(() => {});
}
}, [tab]);
const searchUsers = (q) => {
setSearch(q);
api.adminUsers(q).then(d => setUsers(d.users || [])).catch(() => {});
};
const openUserDetail = async (u) => {
setSelectedUser(u);
setDetailTab('activity');
setLoadingDetail(true);
try {
const d = await api.adminUserDetail(u.id);
setUserDetail(d);
} catch (e) {
toast('Error cargando detalle');
setUserDetail(null);
}
setLoadingDetail(false);
};
const closeDetail = () => { setSelectedUser(null); setUserDetail(null); };
const toggleBlock = async (id, currentStatus) => {
const block = currentStatus !== 'blocked';
try {
await api.adminBlock(id, block);
setUsers(u => u.map(x => x.id === id ? {...x, status: block ? 'blocked' : 'active'} : x));
if (userDetail && userDetail.user.id === id) {
setUserDetail(d => ({...d, user: {...d.user, status: block ? 'blocked' : 'active'}}));
}
toast(block ? 'Usuario bloqueado' : 'Usuario desbloqueado');
} catch (e) { toast('Error: ' + (e.error || 'desconocido')); }
};
const giftPremium = async (id, months) => {
try {
await api.adminGift(id, months);
setUsers(u => u.map(x => x.id === id ? {...x, plan: 'gifted'} : x));
if (userDetail && userDetail.user.id === id) {
setUserDetail(d => ({...d, user: {...d.user, plan: 'gifted', stripe_subscription_id: null}}));
}
toast(months === 0 ? 'Premium vitalicio regalado' : `Premium regalado (${months} ${months===1?'mes':'meses'})`);
setShowGiftFor(null);
setGiftMonths(1);
} catch (e) { toast('Error: ' + (e.error || 'desconocido')); }
};
const doCreateUser = async () => {
if (!newUser.email || !newUser.password || !newUser.name) { toast('Rellena email, contraseña y nombre'); return; }
try {
await api.adminCreateUser(newUser);
toast('Usuario creado');
setShowCreateUser(false);
setNewUser({email:'',password:'',name:'',license:'PPL',plan:'free'});
api.adminUsers(search).then(d => setUsers(d.users || [])).catch(() => {});
} catch (e) { toast('Error: ' + (e.error || 'desconocido')); }
};
const doChangePass = async () => {
if (!newPass || newPass.length < 8) { toast('Mínimo 8 caracteres'); return; }
try {
await api.adminChangePassword(showChangePass, newPass);
toast('Contraseña cambiada');
setShowChangePass(null);
setNewPass('');
} catch (e) { toast('Error: ' + (e.error || 'desconocido')); }
};
const saveIfrRoutes = (routes) => {
setIfrRoutes(routes);
// Persist to DOM script block + localStorage
const el = document.getElementById('ifr-data');
if (el) el.textContent = JSON.stringify(routes);
try { localStorage.setItem('aeroplan_ifr_routes', JSON.stringify(routes)); } catch(e){}
};
const addOrUpdateRoute = () => {
if (!routeForm.from || !routeForm.to || !routeForm.route) { toast('From, To y Ruta son obligatorios'); return; }
let updated;
if (editingRoute !== null) {
updated = ifrRoutes.map((r, i) => i === editingRoute ? {...routeForm} : r);
} else {
updated = [...ifrRoutes, {...routeForm}];
}
saveIfrRoutes(updated);
setEditingRoute(null);
setRouteForm({from:'',to:'',rules:'Z',fl:'',route:'',dur:'',notes:''});
toast(editingRoute !== null ? 'Ruta actualizada' : 'Ruta añadida');
};
const deleteRoute = (idx) => {
saveIfrRoutes(ifrRoutes.filter((_, i) => i !== idx));
toast('Ruta eliminada');
};
const editRoute = (idx) => {
setEditingRoute(idx);
setRouteForm({...ifrRoutes[idx]});
};
const revokePremium = async (id) => {
try {
await api.adminRevoke(id);
setUsers(u => u.map(x => x.id === id ? {...x, plan: 'free', plan_until: null} : x));
if (userDetail && userDetail.user.id === id) {
setUserDetail(d => ({...d, user: {...d.user, plan: 'free', plan_until: null}}));
}
toast('Premium revocado');
} catch (e) { toast('Error: ' + (e.error || 'desconocido')); }
};
const planBadge = (p) => {
const labels = {admin: 'Admin', premium: 'Premium', gifted: 'Gifted', free: 'Free'};
const accent = p === 'premium' || p === 'gifted' || p === 'admin';
return {labels[p] || p};
};
const fmtDate = (ts) => {
if (!ts) return '—';
const d = new Date(ts * 1000);
return d.toLocaleDateString('es', {day: '2-digit', month: 'short', year: 'numeric'}) + ' ' + d.toLocaleTimeString('es', {hour: '2-digit', minute: '2-digit'});
};
const fmtAgo = (ts) => {
if (!ts) return 'nunca';
const diff = Math.floor(Date.now() / 1000) - ts;
if (diff < 60) return 'ahora';
if (diff < 3600) return `hace ${Math.floor(diff / 60)} min`;
if (diff < 86400) return `hace ${Math.floor(diff / 3600)}h`;
return `hace ${Math.floor(diff / 86400)}d`;
};
const levelColor = (l) => l === 'error' ? 'var(--err)' : l === 'warn' ? 'var(--warn)' : 'var(--ok)';
const TABS = [{id:'overview',l:t('admin.overview')},{id:'users',l:t('admin.users')},{id:'ifr',l:t('admin.ifr')},{id:'logs',l:t('admin.logs')},{id:'support',l:t('admin.support')}];
return
{t('admin.panel')}
{t('admin.title')}{' '}
{t('admin.title2')}{' '}
{t('admin.title3')}
{t('admin.subtitle')}
{TABS.map(t=>)}
{/* ===== INSTRUCTOR REQUESTS BANNER (all tabs) ===== */}
{metrics && metrics.instructor_requests > 0 &&
{setTab('users');setUserFilter('instructor');searchUsers('');}}>
{metrics.instructor_requests} solicitud{metrics.instructor_requests>1?'es':''} de instructor pendiente{metrics.instructor_requests>1?'s':''}
Haz clic para revisar y asignar licencia
}
{/* ===== OVERVIEW ===== */}
{tab==='overview' && metrics && <>
{[
{k:'MRR',v:`${metrics.mrr_eur}€`,sub:metrics.paying_users?`${metrics.paying_users} suscriptores`:null},
{k:'Usuarios',v:metrics.users_total},
{k:'Premium',v:metrics.users_premium,sub:metrics.gifted_total?`${metrics.gifted_total} gifted (${metrics.gifted_lifetime} vitalicio${metrics.gifted_temp?`, ${metrics.gifted_temp} temporal`:''})`:null},
{k:'Envíos OK',v:metrics.submissions_ok},
{k:'Envíos Error',v:metrics.submissions_error},
{k:'Bloqueados',v:metrics.users_blocked},
].map((m,i)=>(
{m.k}
{m.v}
{m.sub &&
{m.sub}
}
))}
{metrics.series &&
Envíos (14 días)
{metrics.series.map((s,i)=>{const max=Math.max(...metrics.series.map(x=>x.count),1);const h=(s.count/max)*100;return
})}
}
>}
{/* User detail overlay — rendered at component level so it works from any tab */}
{selectedUser &&
{if(e.target===e.currentTarget) closeDetail();}}>
{/* Header */}
{(userDetail?.user?.name || selectedUser.name || '?')[0]}
{userDetail?.user?.name || selectedUser.name}
{planBadge(userDetail?.user?.plan || selectedUser.plan)}
{(userDetail?.user?.status || selectedUser.status) === 'blocked' && Bloqueado}
{userDetail?.user?.email || selectedUser.email}{(userDetail?.user?.phone || selectedUser.phone) ? ` · ${userDetail?.user?.phone || selectedUser.phone}` : ''} · {userDetail?.user?.license || ''} · Registro {fmtDate(userDetail?.user?.created_at || selectedUser.created_at)}
{userDetail?.user &&
{userDetail.user.plan === 'premium' && userDetail.user.stripe_subscription_id && Stripe activo}
{userDetail.user.plan === 'premium' && !userDetail.user.stripe_subscription_id && Premium (sin Stripe)}
{userDetail.user.plan === 'gifted' && userDetail.user.plan_until && Gift hasta {fmtDate(userDetail.user.plan_until)}}
{userDetail.user.plan === 'gifted' && !userDetail.user.plan_until && Gift vitalicio ♾}
{userDetail.user.plan_until && userDetail.user.plan === 'premium' && · Renueva {fmtDate(userDetail.user.plan_until)}}
}
{/* Actions bar */}
{userDetail &&
{(userDetail.user.plan === 'free' || userDetail.user.plan === 'premium') && }
{(userDetail.user.plan === 'premium' || userDetail.user.plan === 'gifted') && }
{userDetail.user.stripe_subscription_id && }
{userDetail.user.role !== 'admin' && }
}
{/* License selector — dedicated row */}
{userDetail &&
Licencia:
{userDetail.user.license==='INSTRUCTOR_PENDING' && Solicitud pendiente}
}
{/* Stats row */}
{userDetail &&
{[
{k:'Planes',v:userDetail.stats.total_plans},
{k:'Envíos OK',v:userDetail.stats.ok_submissions},
{k:'Errores',v:userDetail.stats.error_submissions},
{k:'Aeronaves',v:userDetail.stats.total_aircraft},
{k:'Último login',v:fmtAgo(userDetail.user.last_login_at)},
].map((s,i)=>
)}
}
{/* Detail tabs */}
{userDetail && <>
{[{id:'activity',l:'Envíos'},{id:'plans',l:'Planes'},{id:'logs',l:'Logs'},{id:'aircraft',l:'Aeronaves'},{id:'tickets',l:'Soporte'}].map(tb=>
)}
{/* Submissions */}
{detailTab==='activity' &&
{userDetail.submissions.length === 0 &&
Sin envíos
}
{userDetail.submissions.map((s,i)=>{
const isOpen = expandedSub === s.id;
let reqFields = null;
try { reqFields = JSON.parse(s.request_json || '{}'); } catch(e){}
return
setExpandedSub(isOpen?null:s.id)}>
{s.plan_name || s.op}
{s.adep && s.ades && {s.adep} → {s.ades}}
{s.icaro_ref && {s.icaro_ref}}
{s.error_message && {s.error_message}}
{fmtDate(s.created_at)}
{s.status.toUpperCase()}
{isOpen && (() => {
const vm = subViewMode[s.id] || 'summary';
const setVm = (m) => setSubViewMode(prev => ({...prev, [s.id]: m}));
// Extract friendly fields from request_json
const r = reqFields || {};
const adep = r['data.departureAerodrome'] || s.adep || '';
const ades = r['data.destinationAerodrome'] || s.ades || '';
const arcid = r['data.flightId'] || '';
const rules = r['data.flightRules'] || '';
const ftype = r['data.flightType'] || '';
const dia = r['dateOfFlight.dia']||''; const mes = r['dateOfFlight.mes']||''; const anio = r['dateOfFlight.anio']||'';
const flightDate = dia && mes ? (dia.toString().padStart(2,'0')+'/'+mes.toString().padStart(2,'0')+(anio?'/'+anio:'')) : '';
const eobtH = r['eobtTime.hora']||''; const eobtM = r['eobtTime.minutos']||'';
const eobt = eobtH ? (eobtH+':'+(eobtM||'00')) : '';
const acType = r['data.aircraftName'] || '';
const wake = r['data.wakeTurbulenceCategory'] || '';
const reg = r['data.aircraftRegistrationMarkings'] || '';
const comEq = r['data.aidEquipment'] || '';
const ssr = r['ssrData'] || '';
const speed = r['data.crusingSpeed'] || '';
const fl = r['data.crusingLevel'] || '';
const route = r['data.route'] || '';
const eetH = r['estimatedElapsedTime.hora']||''; const eetM = r['estimatedElapsedTime.minutos']||'';
const eet = eetH ? (eetH+':'+(eetM||'00')) : '';
const altn = r['data.alternateAerodrome1'] || '';
const altn2 = r['data.alternateAerodrome2'] || '';
const endH = r['fp19.endurance.hora']||''; const endM = r['fp19.endurance.minutos']||'';
const endurance = endH ? (endH+':'+(endM||'00')) : '';
const pob = r['fp19.personsOnBoard'] || '';
const pic = r['fp19.pilot'] || '';
const colors = r['fp19.aircraftColourAndMarkings'] || '';
const eRadio = r['fp19.radio'] || '';
const survival = r['fp19.survivalEquipment'] || '';
const jackets = r['fp19.lifeJackets'] || '';
const rmk = r['data.remarks'] || '';
const opr = r['data.operator'] || '';
const RULES_MAP = {I:'IFR',V:'VFR',Y:'VFR→IFR',Z:'IFR→VFR'};
const FTYPE_MAP = {S:'Regular',N:'No regular',G:'Aviacion general',M:'Militar',X:'Otros'};
const kv = (k,v) => v ?
{k}{v}
: null;
const secHead = (t) =>
{t}
;
return
ID: {s.id}
Duracion: {s.duration_ms}ms
Op: {s.op}
{vm === 'summary' && reqFields && <>
{/* Route header */}
{adep}
→
{ades}
{rules && {RULES_MAP[rules]||rules}}
{fl && {fl}}
{acType && {acType}}
{eet && {eet}}
{flightDate && {flightDate}}
{/* Identificacion */}
{secHead('Identificacion')}
{kv('ARCID', arcid)}
{kv('Reglas', RULES_MAP[rules]||rules)}
{kv('Tipo vuelo', FTYPE_MAP[ftype]||ftype)}
{kv('Fecha', flightDate)}
{/* Aeronave */}
{secHead('Aeronave')}
{kv('Tipo', acType)}
{kv('Estela', wake)}
{kv('REG', reg)}
{kv('Equipo COM', comEq)}
{kv('SSR', ssr)}
{/* Ruta */}
{secHead('Ruta')}
{kv('EOBT', eobt ? eobt+'Z' : '')}
{kv('Velocidad', speed)}
{kv('Nivel', fl)}
{kv('Ruta', route)}
{kv('EET', eet)}
{kv('ALTN', altn)}
{kv('ALTN2', altn2)}
{/* Suplementario */}
{(endurance || pob || pic || colors || eRadio || survival) &&
{secHead('Suplementario')}
{kv('Autonomia', endurance)}
{kv('POB', pob)}
{kv('PIC', pic)}
{kv('Color/Marcas', colors)}
{kv('Radio emergencia', eRadio)}
{kv('Supervivencia', survival)}
{kv('Chalecos', jackets)}
}
{/* Observaciones */}
{(rmk || opr) &&
{secHead('Observaciones')}
{kv('RMK', rmk)}
{kv('OPR', opr)}
}
>}
{vm === 'summary' && !reqFields &&
Sin datos de peticion
}
{vm === 'post' && <>
Peticion POST enviada a ENAIRE
{reqFields &&
{Object.entries(reqFields).filter(([k,v])=>v).sort(([a],[b])=>a.localeCompare(b)).map(([k,v])=>
{k}
{v}
)}
}
{!reqFields &&
Sin datos de peticion
}
>}
{/* Respuesta ENAIRE */}
{s.response_json &&
Respuesta ENAIRE (HTML)
{(() => {
try { const r2 = JSON.parse(s.response_json); return r2.raw || JSON.stringify(r2, null, 2); } catch(e) { return s.response_json; }
})()}
}
;
})()}
;
})}
}
{/* Plans */}
{detailTab==='plans' &&
{userDetail.plans.length === 0 &&
Sin planes
}
{userDetail.plans.map((p,i)=>
{p.favorite ?
: }
{p.name}
{p.adep} → {p.ades}
{p.flight_rules} · {p.aircraft_type}
{fmtDate(p.updated_at)}
)}
}
{/* Logs */}
{detailTab==='logs' &&
{userDetail.logs.length === 0 &&
Sin logs
}
{userDetail.logs.map((l,i)=>
{l.event}
{l.message && {l.message}}
{l.ip && {l.ip}}
{fmtDate(l.created_at)}
{l.level.toUpperCase()}
)}
}
{/* Aircraft */}
{detailTab==='aircraft' &&
{userDetail.aircraft.length === 0 &&
Sin aeronaves
}
{userDetail.aircraft.map((a,i)=>
{a.registration}
{a.type}
Estela {a.wake}
{a.is_default ? Default : null}
)}
}
{/* User tickets */}
{detailTab==='tickets' &&
{(!userDetail.tickets || userDetail.tickets.length === 0) &&
Sin tickets de soporte
}
{(userDetail.tickets || []).map((tk,i)=>
{tk.status === 'open' ? 'Abierto' : tk.status === 'replied' ? 'Respondido' : 'Cerrado'}
{fmtDate(tk.created_at)}
{tk.message}
{tk.admin_reply &&
Admin:{tk.admin_reply}
}
)}
}
>}
{loadingDetail &&
Cargando...
}
}
{/* ===== USERS ===== */}
{tab==='users' && (() => {
const filtered = users.filter(u => {
if (userFilter === 'all') return true;
if (userFilter === 'premium') return u.plan === 'premium';
if (userFilter === 'gifted') return u.plan === 'gifted';
if (userFilter === 'free') return u.plan === 'free';
if (userFilter === 'blocked') return u.status === 'blocked';
if (userFilter === 'instructor') return u.license === 'INSTRUCTOR_PENDING';
return true;
});
return <>
{/* User list */}
Usuarios ({filtered.length}{userFilter!=='all'?` / ${users.length}`:''})
{users.filter(u=>u.status==='active').length} activos · {users.filter(u=>u.plan==='premium'||u.plan==='gifted').length} premium · {users.filter(u=>u.status==='blocked').length} bloqueados
{/* Filter bar */}
{[
{id:'all',l:'Todos',count:users.length},
{id:'free',l:'Free',count:users.filter(u=>u.plan==='free').length},
{id:'premium',l:'Premium',count:users.filter(u=>u.plan==='premium').length},
{id:'gifted',l:'Gifted',count:users.filter(u=>u.plan==='gifted').length},
{id:'blocked',l:'Bloqueados',count:users.filter(u=>u.status==='blocked').length},
{id:'instructor',l:'📋 Pendientes',count:users.filter(u=>u.license==='INSTRUCTOR_PENDING').length},
].filter(f=>f.id==='all'||f.count>0).map(f=>)}
{filtered.map((u,i)=>
openUserDetail(u)}
onMouseEnter={e=>e.currentTarget.style.background='var(--bg-inset)'}
onMouseLeave={e=>e.currentTarget.style.background=''}
>
{u.name[0]}
{u.name}
{u.email}{u.phone ? ` · ${u.phone}` : ''} · {u.license || '—'} · {fmtAgo(u.last_login_at)}
{planBadge(u.plan)}
{u.license === 'INSTRUCTOR_PENDING' &&
Instructor pendiente}
{u.status === 'blocked' &&
Bloqueado}
e.stopPropagation()}>
{u.plan === 'free' && }
{(u.plan === 'premium' || u.plan === 'gifted') && }
{u.role !== 'admin' && }
)}
>;
})()}
{/* ===== IFR ROUTES ===== */}
{tab==='ifr' && <>
Rutas IFR ({ifrRoutes.length})
{ifrRoutes.length === 0 &&
Sin rutas
}
{ifrRoutes.map((r,i)=>
{r.from} - {r.to}
{r.rules}
{r.fl}
{r.route}
{r.dur}
)}
>}
{/* ===== LOGS ===== */}
{tab==='logs' &&
Actividad
{logs.length} registros
{['','info','warn','error'].map(l=>)}
{logs.map((l,i)=>
{fmtDate(l.created_at)}
{l.event}
{l.message && {l.message}}
{l.ip && {l.ip}}
{l.level.toUpperCase()}
)}
{logs.length === 0 &&
Sin logs
}
}
{/* ===== SUPPORT ===== */}
{tab==='support' && (() => {
const openTk = tickets.filter(x => x.status !== 'closed');
const closedTk = tickets.filter(x => x.status === 'closed');
return <>
{t('admin.support')}
{supportFilter==='open' && <>
{openTk.length === 0 &&
Sin tickets abiertos
}
{openTk.map(tk => {
const isOpen = expandedTk[tk.id] !== false; // open by default for open tickets
const lastMsg = (tk.messages||[]).slice(-1)[0];
return
{/* Collapsible header */}
setExpandedTk(s=>({...s,[tk.id]:!isOpen}))}>
{(tk.user_name||'?')[0]}
{tk.user_name || 'Usuario'}
{!isOpen && lastMsg &&
{lastMsg.sender==='admin'?'Tú: ':''}{lastMsg.body}
}
{tk.status === 'replied' ? 'Respondido' : 'Abierto'}
{(tk.messages||[]).length} msg
{/* Expanded content */}
{isOpen &&
{/* User info bar */}
{e.stopPropagation();openUserDetail({id:tk.user_id,name:tk.user_name,email:tk.user_email,plan:tk.user_plan,license:tk.user_license,status:tk.user_status});}}>{tk.user_email} · {tk.user_license || '—'} · {planBadge(tk.user_plan)}
{fmtDate(tk.created_at)}
{/* Conversation messages */}
{(tk.messages || []).map((m, i) =>
{m.sender === 'admin' &&
AeroPlan
}
{m.body}
{new Date(m.created_at * 1000).toLocaleString()}
)}
{/* Reply + actions */}
}
;
})}
>}
{supportFilter==='closed' && <>
{closedTk.length === 0 &&
Sin tickets cerrados
}
{closedTk.map(tk => {
const isOpen = !!expandedTk[tk.id];
const firstMsg = (tk.messages||[])[0];
return
{/* Collapsible header */}
setExpandedTk(s=>({...s,[tk.id]:!isOpen}))}>
{(tk.user_name||'?')[0]}
{tk.user_name || 'Usuario'}
{!isOpen && firstMsg &&
{firstMsg.body}
}
{(tk.messages||[]).length} msg · {fmtDate(tk.created_at)}
{/* Expanded content */}
{isOpen &&
{/* User info bar */}
{e.stopPropagation();openUserDetail({id:tk.user_id,name:tk.user_name,email:tk.user_email,plan:tk.user_plan,license:tk.user_license,status:tk.user_status});}}>{tk.user_email} · {tk.user_license || '—'} · {planBadge(tk.user_plan)}
{fmtDate(tk.created_at)}
{/* Chat bubbles */}
{(tk.messages || []).map((m, i) =>
{m.sender === 'admin' &&
AeroPlan
}
{m.body}
{new Date(m.created_at * 1000).toLocaleString()}
)}
}
;
})}
>}
>;
})()}
{/* ===== GIFT PREMIUM MODAL ===== */}
{showGiftFor &&
{if(e.target===e.currentTarget) setShowGiftFor(null);}}>
Regalar Premium
{[1,3,6,12].map(m=>)}
0 ? giftMonths : ''} onChange={e=>setGiftMonths(Math.max(1,parseInt(e.target.value)||1))} style={{width:80,textAlign:'center'}} placeholder="N"/>
meses
o
{giftMonths === 0 &&
Sin fecha de expiración. Cancela la suscripción Stripe si existe.
}
}
{/* ===== CREATE USER MODAL ===== */}
{showCreateUser &&
{if(e.target===e.currentTarget) setShowCreateUser(false);}}>
Crear usuario
}
{/* ===== CHANGE PASSWORD MODAL ===== */}
{showChangePass &&
{if(e.target===e.currentTarget) setShowChangePass(null);}}>
}
;
};
window.Admin = Admin;