/* ============================================================
page-support.jsx — Support ticket management tab ?v=8 (vpsie connect modal)
============================================================ */
(function () {
const { useState, useEffect, useCallback, useRef } = React;
const parseDetail = async (url, opts) => {
const r = await fetch(url, { credentials: 'include', ...opts });
if (!r.ok) throw new Error(r.status);
return r.json();
};
/* ── helpers ─────────────────────────────────────── */
function fmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function fmtRelative(iso) {
if (!iso) return '';
const diff = Date.now() - new Date(iso).getTime();
const s = Math.floor(diff / 1000);
/* future timestamps or <60s → "just now" */
const ar = window.appLang && window.appLang() === 'ar';
if (s < 60) return ar ? 'الآن' : 'just now';
const m = Math.floor(s / 60);
if (m < 60) return ar ? `قبل ${m} د` : `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return ar ? `قبل ${h} س` : `${h}h ago`;
return ar ? `قبل ${Math.floor(h / 24)} ي` : `${Math.floor(h / 24)}d ago`;
}
function fmtMin(min) {
if (min == null) return '—';
if (min < 60) return `${Math.round(min)}m`;
return `${(min / 60).toFixed(1)}h`;
}
function initials(name) {
if (!name) return '?';
return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
}
/* ── avatar ──────────────────────────────────────── */
function Avatar({ name, size = 28 }) {
const colors = ['#6366f1','#8b5cf6','#ec4899','#0ea5e9','#10b981','#f59e0b'];
const i = name ? name.charCodeAt(0) % colors.length : 0;
return (
{initials(name)}
);
}
/* ── severity pill ───────────────────────────────── */
function SevPill({ sev }) {
const T = window.appT || (s => s);
const MAP = {
low: { bg: '#10b981', label: T('Low') },
medium: { bg: '#f59e0b', label: T('Medium') },
high: { bg: '#ef4444', label: T('High') },
};
const { bg, label } = MAP[sev] || { bg: '#6b7280', label: sev || '—' };
return (
{label}
);
}
/* ── status pill ─────────────────────────────────── */
function StatusPill({ st }) {
const T = window.appT || (s => s);
const MAP = {
open: { bg: '#6b7280', label: T('Open') },
acknowledged: { bg: '#3b82f6', label: T('Acknowledged') },
in_progress: { bg: '#f59e0b', label: T('In Progress') },
resolved: { bg: '#10b981', label: T('Resolved') },
closed: { bg: '#475569', label: T('Closed') },
};
const { bg, label } = MAP[st] || { bg: '#6b7280', label: st || '—' };
return (
{label}
);
}
/* ── toast ───────────────────────────────────────── */
function Toast({ msg, type = 'ok', onDone }) {
useEffect(() => {
const t = setTimeout(onDone, 2800);
return () => clearTimeout(t);
}, []);
const bg = type === 'err' ? '#ef4444' : '#10b981';
return (
{msg}
);
}
/* ── stats strip ─────────────────────────────────── */
function StatsStrip({ stats }) {
const st = stats || {};
const noData = stats === null;
const T = window.appT || (s => s);
const items = [
{ label: T('Open'), value: noData ? '—' : (st.open ?? 0), accent: '#6b7280', topBar: '#6b7280' },
{ label: T('Acknowledged'), value: noData ? '—' : (st.acknowledged ?? 0), accent: '#3b82f6', topBar: '#3b82f6' },
{ label: T('Resolved this week'),value: noData ? '—' : (st.resolved_this_week ?? 0), accent: '#10b981', topBar: '#10b981' },
{ label: T('Avg response'), value: fmtMin(st.avg_response_min), accent: st.avg_response_min != null ? 'var(--text-1)' : 'var(--text-3)', topBar: '#8b5cf6' },
];
return (
{items.map(it => (
{
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.25)';
}}
onMouseLeave={e => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
{/* top accent bar */}
{it.value}
{it.label}
))}
);
}
/* ── severity segmented control ─────────────────── */
function SevSegmented({ value, onChange }) {
const T = window.appT || (s => s);
const opts = [
{ value: 'low', label: T('Low'), bg: '#10b981' },
{ value: 'medium', label: T('Medium'), bg: '#f59e0b' },
{ value: 'high', label: T('High'), bg: '#ef4444' },
];
return (
{opts.map(o => (
onChange(o.value)}
style={{
flex: 1,
padding: '7px 12px',
borderRadius: 8,
border: value === o.value ? 'none' : '1px solid var(--line)',
background: value === o.value ? o.bg : 'var(--bg-elev-2)',
color: value === o.value ? '#fff' : 'var(--text-3)',
fontSize: 12, fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.15s',
letterSpacing: '0.03em',
}}
>{o.label}
))}
);
}
/* ── VPSie connect modal ─────────────────────────── */
function VpsieConnectModal({ onClose, onConnected }) {
const [clientId, setClientId] = useState('');
const [clientSecret, setClientSecret] = useState('');
const [busy, setBusy] = useState(false);
const [err, setErr] = useState(null);
async function submit(e) {
e && e.preventDefault();
const cid = clientId.trim(), sec = clientSecret.trim();
const T = window.appT || (s => s);
if (!cid || !sec) { setErr(T('Both Client ID and Client Secret are required.')); return; }
setErr(null); setBusy(true);
try {
const r = await fetch('/api/me/vpsie/connect', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: cid, client_secret: sec }),
});
if (!r.ok) {
let msg = `HTTP ${r.status}`;
try { const j = await r.json(); msg = j.detail || j.message || msg; } catch(_) {}
setErr(typeof msg === 'string' ? msg : T('Could not validate those credentials.'));
setBusy(false);
return;
}
// Success — bust cache and notify parent
await fetch('/api/me/vpsie/refresh', { method: 'POST', credentials: 'include' }).catch(()=>{});
setBusy(false);
onConnected && onConnected();
onClose && onClose();
} catch (_e) {
setErr(T('Network error — please try again.'));
setBusy(false);
}
}
const overlay = {
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 9999,
};
const panel = {
width: 'min(480px, 92vw)', background: 'var(--bg-elev-1)',
border: '1px solid var(--line)', borderRadius: 12,
padding: '22px 22px 18px', boxShadow: '0 24px 60px rgba(0,0,0,0.45)',
};
const inputStyle = {
width: '100%', boxSizing: 'border-box', padding: '9px 12px',
background: 'var(--bg-1)', border: '1px solid var(--line)',
borderRadius: 6, color: 'var(--text-1)', fontSize: 13,
fontFamily: 'var(--font-mono, monospace)',
};
const labelStyle = { display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 5 };
return (
{ if (e.target === e.currentTarget && !busy) onClose && onClose(); }}>
{(window.appT||(s=>s))("Connect your VPSie account")}
{(window.appT||(s=>s))("Your client ID + secret are stored encrypted and only used to fetch")}
{(window.appT||(s=>s))(" your ")} {(window.appT||(s=>s))("servers.")}
{' '}{(window.appT||(s=>s))("Admins cannot see them. Generate creds at")}{' '}
my.vpsie.com/account/api .
{err && (
{err}
)}
);
}
/* ── new ticket form (VPSie-inspired layout) ─────── */
function NewTicketForm({ onCreated }) {
const [subject, setSubject] = useState('');
const [relatedServer, setRelatedSvr] = useState('');
const [relatedSvrData, setRelatedSvrData] = useState(null);
const [category, setCategory] = useState('');
const [severity, setSeverity] = useState('medium');
const [issue, setIssue] = useState('');
const [files, setFiles] = useState([]);
const [showVpsieModal, setShowVpsieModal] = useState(false);
const [vpsieReloadKey, setVpsieReloadKey] = useState(0);
const [vpsieToast, setVpsieToast] = useState(null);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState(null);
const [ok, setOk] = useState(null);
const fileInputRef = useRef(null);
function onFilesChange(e) {
const picked = Array.from(e.target.files || []);
// Cap individual file size and total selection at sensible limits
const MAX_BYTES = 25 * 1024 * 1024;
const valid = picked.filter(f => f.size <= MAX_BYTES);
if (valid.length < picked.length) {
setErr((window.appT||(s=>s))('One or more files exceed the 25 MB per-file limit.'));
}
setFiles(prev => [...prev, ...valid]);
// Reset input so the same file can be re-selected if removed
if (fileInputRef.current) fileInputRef.current.value = '';
}
function removeFile(idx) {
setFiles(prev => prev.filter((_, i) => i !== idx));
}
function fmtBytes(n) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
async function submit(e) {
e.preventDefault();
if (!issue.trim()) { setErr((window.appT||(s=>s))('Issue details are required.')); return; }
setErr(null); setLoading(true);
try {
// Category value is "Label|@bot.matrix_id" — split it
const [catLabel, assignedBot] = (category || '').split('|');
// Compose backend payload — prepend subject + related-server context to issue body
const composedIssue = [
subject.trim() && `Subject: ${subject.trim()}`,
relatedSvrData ? `Related server: hostname=${relatedSvrData.hostname}, ip=${relatedSvrData.ip}, status=${relatedSvrData.status}, region=${relatedSvrData.region}` : (relatedServer.trim() && `Related server: ${relatedServer.trim()}`),
issue.trim(),
].filter(Boolean).join('\n\n');
const res = await parseDetail('/api/support/escalate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
team_name: catLabel || null,
severity,
issue: composedIssue,
assigned_bot_matrix_id: assignedBot || null,
}),
});
// Upload attachments (if any) — fire-and-forget; backend posts to room
if (files.length > 0 && res?.id) {
try {
const fd = new FormData();
files.forEach(f => fd.append('files', f, f.name));
await fetch(`/api/support/tickets/${res.id}/attachments`, {
method: 'POST', credentials: 'include', body: fd,
});
} catch (_e) { /* don't block ticket creation on attachment failure */ }
}
setOk(res.ticket_number);
setSubject(''); setRelatedSvr(''); setRelatedSvrData(null); setCategory(''); setSeverity('medium'); setIssue(''); setFiles([]);
onCreated && onCreated();
} catch (_e) {
setErr((window.appT||(s=>s))('Failed to create ticket. Please try again.'));
} finally {
setLoading(false);
}
}
const fieldLabel = {
position: 'absolute', top: -8, left: 12, padding: '0 6px',
background: 'var(--bg-elev-1)', fontSize: 11, fontWeight: 600,
color: 'var(--text-3)', letterSpacing: '0.04em', textTransform: 'uppercase',
pointerEvents: 'none',
};
const fieldWrap = { position: 'relative', flex: 1 };
const selectStyle = {
width: '100%', boxSizing: 'border-box',
appearance: 'none', WebkitAppearance: 'none',
backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'6\' viewBox=\'0 0 10 6\'%3E%3Cpath fill=\'%2394a3b8\' d=\'M0 0l5 6 5-6z\'/%3E%3C/svg%3E")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 14px center',
paddingRight: 36,
};
return (
{showVpsieModal && (
setShowVpsieModal(false)}
onConnected={() => {
setVpsieReloadKey(k => k + 1);
setVpsieToast((window.appT||(s=>s))('VPSie connected. Server list refreshed.'));
setTimeout(() => setVpsieToast(null), 4000);
}}
/>
)}
{vpsieToast && (
{vpsieToast}
)}
{ok && (
{(window.appT||(s=>s))("Ticket")} {ok} {(window.appT||(s=>s))("created and posted to the support room.")}
)}
{err && (
{err}
)}
);
}
/* ── status filter segmented control ────────────── */
function StatusFilterSeg({ value, onChange }) {
const T = window.appT || (s => s);
const opts = [
{ value: 'open', label: T('Open') },
{ value: 'acknowledged', label: T('Acknowledged') },
{ value: 'in_progress', label: T('In Progress') },
{ value: 'resolved', label: T('Resolved') },
{ value: 'closed', label: T('Closed') },
{ value: 'all', label: T('All') },
];
return (
{opts.map(o => (
onChange(o.value)}
style={{
padding: '5px 12px',
borderRadius: 99,
border: value === o.value ? 'none' : '1px solid var(--line)',
background: value === o.value ? 'var(--brand)' : 'var(--bg-elev-2)',
color: value === o.value ? '#fff' : 'var(--text-2)',
fontSize: 12, fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.12s',
}}
>{o.label}
))}
);
}
/* ── assigned-to-me toggle pill ──────────────────── */
function AssignedToggle({ value, onChange }) {
return (
onChange(!value)}
style={{
padding: '5px 14px',
borderRadius: 99,
border: value ? 'none' : '1px solid var(--line)',
background: value ? '#10b981' : 'var(--bg-elev-2)',
color: value ? '#fff' : 'var(--text-2)',
fontSize: 12, fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.12s',
display: 'flex', alignItems: 'center', gap: 5,
}}
>
{value && (
)}
{(window.appT||(s=>s))("Assigned to me")}
);
}
/* ── ticket drawer ───────────────────────────────── */
function TicketDrawer({ ticket, onClose, onUpdated }) {
const [detail, setDetail] = useState(null);
const [comment, setComment] = useState('');
const [posting, setPosting] = useState(false);
const [updating, setUpdating] = useState(false);
const [toast, setToast] = useState(null);
const drawerRef = useRef(null);
/* load detail */
useEffect(() => {
parseDetail(`/api/support/tickets/${ticket.id}`)
.then(setDetail)
.catch(() => {});
}, [ticket.id]);
/* ESC to close */
useEffect(() => {
function onKey(e) { if (e.key === 'Escape') onClose(); }
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
/* prevent body scroll when open */
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);
const current = detail || ticket;
async function patchStatus(newStatus) {
setUpdating(true);
try {
await parseDetail(`/api/support/tickets/${ticket.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
});
onUpdated && onUpdated();
const d = await parseDetail(`/api/support/tickets/${ticket.id}`);
setDetail(d);
const ar = window.appLang && window.appLang() === 'ar';
const T = window.appT || (s => s);
const stLabel = { open: T('Open'), acknowledged: T('Acknowledged'), in_progress: T('In Progress'), resolved: T('Resolved'), closed: T('Closed') }[newStatus] || newStatus.replace('_', ' ');
setToast({ msg: ar ? `تم تحديث الحالة إلى ${stLabel}` : `Status updated to ${newStatus.replace('_', ' ')}`, type: 'ok' });
} catch (_e) {
setToast({ msg: (window.appT||(s=>s))('Failed to update status'), type: 'err' });
} finally { setUpdating(false); }
}
async function postComment(e) {
e.preventDefault();
if (!comment.trim()) return;
setPosting(true);
try {
await parseDetail(`/api/support/tickets/${ticket.id}/comment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: comment.trim() }),
});
setComment('');
const d = await parseDetail(`/api/support/tickets/${ticket.id}`);
setDetail(d);
} catch (_e) {
setToast({ msg: (window.appT||(s=>s))('Failed to post comment'), type: 'err' });
} finally { setPosting(false); }
}
/* action buttons config */
const _T = window.appT || (s => s);
const ACTION_BTNS = [
{ status: 'acknowledged', label: _T('Acknowledge'), already: _T('Already acknowledged'), bg: '#3b82f6' },
{ status: 'in_progress', label: _T('Mark In Progress'), already: _T('Already in progress'), bg: '#f59e0b' },
{ status: 'resolved', label: _T('Resolve'), already: _T('Already resolved'), bg: '#10b981' },
{ status: 'closed', label: _T('Close'), already: _T('Already closed'), bg: '#475569' },
];
/* timeline entries */
const _Tt = window.appT || (s => s);
const timeline = [
ticket.created_at && { ts: ticket.created_at, label: _Tt('Created'), by: ticket.submitted_by_name, color: '#6b7280' },
current.acknowledged_at && { ts: current.acknowledged_at, label: _Tt('Acknowledged'), by: current.assigned_to_name, color: '#3b82f6' },
current.resolved_at && { ts: current.resolved_at, label: _Tt('Resolved'), by: current.assigned_to_name, color: '#10b981' },
].filter(Boolean);
const comments = detail?.comments || [];
return (
{/* backdrop — deeper blur + higher opacity */}
{/* panel */}
{/* header bar */}
{ticket.ticket_number}
s))("Close (Esc)")}
style={{ fontSize: 16, lineHeight: 1, padding: '4px 8px' }}
>✕
{/* scrollable body */}
{/* title + pills row */}
{ticket.team_name || (window.appT||(s=>s))('Support Ticket')}
{
parseDetail(`/api/support/tickets/${ticket.id}`)
.then(setDetail)
.catch(() => {});
}}
title={(window.appT||(s=>s))("Refresh")}
>↻
{/* metadata grid */}
{(window.appT||(s=>s))("Submitted by")}
{ticket.submitted_by_name || '—'}
{(window.appT||(s=>s))("Assigned to")}
{current.assigned_to_name
? <>
{current.assigned_to_name} >
:
{(window.appT||(s=>s))("Unassigned")}
}
{(window.appT||(s=>s))("Created")}
{fmtDate(ticket.created_at)}
{ticket.created_at && ({fmtRelative(ticket.created_at)}) }
{(window.appT||(s=>s))("Last updated")}
{current.updated_at
? <>{fmtDate(current.updated_at)} ({fmtRelative(current.updated_at)}) >
: '—'}
{/* issue card */}
{(window.appT||(s=>s))("Issue")}
{ticket.issue}
{/* resolution notes */}
{detail?.resolution_notes && (
{(window.appT||(s=>s))("Resolution notes")}
{detail.resolution_notes}
)}
{/* divider before actions */}
{/* action buttons */}
{(window.appT||(s=>s))("Actions")}
{ACTION_BTNS.map(a => {
const isCurrentStatus = a.status === current.status;
return (
!isCurrentStatus && patchStatus(a.status)}
disabled={updating || isCurrentStatus}
title={isCurrentStatus ? a.already : undefined}
style={{
padding: '7px 14px',
borderRadius: 8,
border: 'none',
cursor: isCurrentStatus || updating ? 'not-allowed' : 'pointer',
background: isCurrentStatus ? 'var(--bg-elev-2,#374151)' : a.bg,
color: isCurrentStatus ? 'var(--text-3)' : '#fff',
fontSize: 12.5,
fontWeight: 600,
opacity: updating ? 0.6 : 1,
transition: 'opacity 0.15s, transform 0.1s, filter 0.1s',
}}
onMouseEnter={e => { if (!isCurrentStatus && !updating) { e.currentTarget.style.filter = 'brightness(1.15)'; e.currentTarget.style.transform = 'scale(1.02)'; } }}
onMouseLeave={e => { e.currentTarget.style.filter = ''; e.currentTarget.style.transform = ''; }}
>
{isCurrentStatus ? a.already : a.label}
);
})}
{/* status timeline */}
{timeline.length > 0 && (
{(window.appT||(s=>s))("Timeline")}
{timeline.map((entry, idx) => (
{idx < timeline.length - 1 && (
)}
{entry.label}
{entry.by &&
{(window.appT||(s=>s))("by")} {entry.by} }
{fmtDate(entry.ts)} · {fmtRelative(entry.ts)}
))}
)}
{/* comments */}
{(window.appT||(s=>s))("Comments")} ({comments.length})
{/* empty comments state */}
{comments.length === 0 && (
{/* chat bubble icon */}
{(window.appT||(s=>s))("No comments yet · be the first to add one")}
)}
{comments.map(c => (
{c.author_name}
{fmtDate(c.created_at)} · {fmtRelative(c.created_at)}
{c.body}
))}
setComment(e.target.value)}
placeholder={(window.appT||(s=>s))("Add a comment…")}
onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) postComment(e); }}
/>
{posting ? '…' : (window.appT||(s=>s))('Post comment')}
{/* toast */}
{toast &&
setToast(null)} />}
);
}
/* ── tickets table ───────────────────────────────── */
function TicketsTable({ tickets, onSelect }) {
const safeTickets = tickets || [];
if (safeTickets.length === 0) {
return (
{/* clipboard icon */}
{(window.appT||(s=>s))("You currently don't have any support tickets.")}
{(window.appT||(s=>s))("Use the form above to create your first ticket.")}
);
}
return (
{[(window.appT||(s=>s))('Ticket #'), (window.appT||(s=>s))('Team'), (window.appT||(s=>s))('Severity'), (window.appT||(s=>s))('Status'), (window.appT||(s=>s))('Submitted by'), (window.appT||(s=>s))('Created'), ''].map((h, hi) => (
{h}
))}
{safeTickets.map((t, idx) => (
onSelect(t)}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
onMouseLeave={e => e.currentTarget.style.background = idx % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.015)'}
>
{t.ticket_number}
{t.team_name || '—'}
{t.submitted_by_name || '—'}
{fmtDate(t.created_at)}
{/* ChevronRight icon instead of "View" button */}
))}
);
}
/* ── main SupportPage ────────────────────────────── */
function SupportPage() {
const [stats, setStats] = useState(null);
const [tickets, setTickets] = useState([]);
const [loading, setLoading] = useState(true);
const [filterStatus, setFilterStatus] = useState("all");
const [filterTeam, setFilterTeam] = useState('');
const [assignedMe, setAssignedMe] = useState(false);
const [selected, setSelected] = useState(null);
const anyFilterActive = filterStatus !== 'all' || filterTeam !== '' || assignedMe;
const reload = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (filterStatus !== 'all') params.set('status', filterStatus);
if (filterTeam) params.set('team', filterTeam);
if (assignedMe) params.set('assigned', 'me');
params.set('limit', '50');
const [s, t] = await Promise.all([
parseDetail('/api/support/tickets/stats').catch(() => null),
parseDetail('/api/support/tickets?' + params.toString()),
]);
setStats(s);
setTickets(Array.isArray(t) ? t : (t?.tickets ?? []));
} catch (_e) {
setTickets([]);
} finally {
setLoading(false);
}
}, [filterStatus, filterTeam, assignedMe]);
useEffect(() => { reload(); }, [reload]);
return (
{/* styles */}
{(window.appT||(s=>s))("Support")}
{(window.appT||(s=>s))("Track and resolve escalated support tickets")}
s))("Refresh")} style={{ fontSize: 13 }}>
{loading ? '…' : '↻ ' + (window.appT||(s=>s))('Refresh')}
{/* Manual ticket/escalation form removed — tickets are ingested from the
VPSie support room and escalation happens automatically up the
on-call chain (primary → backup → escalation). */}
{/* filters */}
{(window.appT||(s=>s))("Status")}
s))("Team…")} value={filterTeam} onChange={e => setFilterTeam(e.target.value)} />
{/* clear filters */}
{anyFilterActive && (
{ setFilterStatus('all'); setFilterTeam(''); setAssignedMe(false); }}
style={{
marginLeft: 'auto',
padding: '5px 12px',
borderRadius: 99,
border: '1px solid var(--line)',
background: 'transparent',
color: 'var(--text-3)',
fontSize: 12, fontWeight: 600,
cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 5,
}}
title={(window.appT||(s=>s))("Clear all filters")}
>
{(window.appT||(s=>s))("Clear")}
)}
{/* Support Cases section header */}
{(window.appT||(s=>s))("Support Cases")}
{!loading && tickets && (
{tickets.length} {tickets.length === 1 ? (window.appT||(s=>s))('case') : (window.appT||(s=>s))('cases')}
)}
{/* tickets table card */}
{loading
?
{(window.appT||(s=>s))("Loading tickets…")}
:
}
{selected && (
setSelected(null)}
onUpdated={reload}
/>
)}
);
}
window.SupportPage = SupportPage;
})();