@microcosmmoney/portal-react 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  export interface MicrocosmQueueStatusPageProps {
2
2
  basePath?: string;
3
3
  onNavigate?: (path: string) => void;
4
+ isAdmin?: boolean;
4
5
  }
5
- export declare function MicrocosmQueueStatusPage({}?: MicrocosmQueueStatusPageProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function MicrocosmQueueStatusPage({ isAdmin }?: MicrocosmQueueStatusPageProps): import("react/jsx-runtime").JSX.Element;
@@ -13,9 +13,11 @@ const LEVEL_LABELS = {
13
13
  warden: 'Warden',
14
14
  admiral: 'Admiral',
15
15
  };
16
- function MicrocosmQueueStatusPage({} = {}) {
16
+ function MicrocosmQueueStatusPage({ isAdmin = false } = {}) {
17
17
  const api = (0, auth_react_1.useMicrocosmApi)();
18
18
  const [userQueue, setUserQueue] = (0, react_1.useState)(null);
19
+ const [adminQueue, setAdminQueue] = (0, react_1.useState)(null);
20
+ const [expansion, setExpansion] = (0, react_1.useState)(null);
19
21
  const [loading, setLoading] = (0, react_1.useState)(true);
20
22
  const [submitting, setSubmitting] = (0, react_1.useState)(false);
21
23
  const [showJoinConfirm, setShowJoinConfirm] = (0, react_1.useState)(false);
@@ -34,7 +36,45 @@ function MicrocosmQueueStatusPage({} = {}) {
34
36
  finally {
35
37
  setLoading(false);
36
38
  }
37
- }, [api]);
39
+ if (isAdmin) {
40
+ try {
41
+ const adminRes = await api.get('/territories/queue/admin');
42
+ setAdminQueue(adminRes?.data ?? adminRes);
43
+ }
44
+ catch { }
45
+ try {
46
+ const expRes = await api.get('/territories/expansion/check');
47
+ setExpansion(expRes?.data ?? expRes);
48
+ }
49
+ catch { }
50
+ }
51
+ }, [api, isAdmin]);
52
+ const handleProcessQueue = async () => {
53
+ setSubmitting(true);
54
+ try {
55
+ await api.post('/territories/queue/process', { batch_size: 50 });
56
+ await loadStatus();
57
+ }
58
+ catch (e) {
59
+ setError(e instanceof Error ? e.message : 'Failed to process queue');
60
+ }
61
+ finally {
62
+ setSubmitting(false);
63
+ }
64
+ };
65
+ const handleTriggerExpansion = async () => {
66
+ setSubmitting(true);
67
+ try {
68
+ await api.post('/territories/expansion/trigger', {});
69
+ await loadStatus();
70
+ }
71
+ catch (e) {
72
+ setError(e instanceof Error ? e.message : 'Failed to trigger expansion');
73
+ }
74
+ finally {
75
+ setSubmitting(false);
76
+ }
77
+ };
38
78
  (0, react_1.useEffect)(() => { loadStatus(); }, [loadStatus]);
39
79
  const handleJoin = async () => {
40
80
  setSubmitting(true);
@@ -69,5 +109,5 @@ function MicrocosmQueueStatusPage({} = {}) {
69
109
  }
70
110
  const level = userQueue?.user_type?.toLowerCase() || 'miner';
71
111
  const levelLabel = LEVEL_LABELS[level] || 'Miner';
72
- return ((0, jsx_runtime_1.jsxs)("div", { className: "max-w-7xl mx-auto px-3 py-4 space-y-3 xs:px-4 xs:space-y-4 sm:px-6 sm:py-6 sm:space-y-6 font-mono", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-lg sm:text-2xl font-bold text-white tracking-wider", children: "Station Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs sm:text-sm text-neutral-400", children: "Your onboarding status" })] }), (0, jsx_runtime_1.jsx)("button", { onClick: loadStatus, className: "px-3 py-1.5 text-xs border border-neutral-700 text-neutral-400 hover:bg-neutral-800 hover:text-white rounded transition-colors", children: "Refresh" })] }), error && ((0, jsx_runtime_1.jsx)("div", { className: "p-3 bg-red-900/20 border border-red-800 rounded text-sm text-red-300", children: error })), (0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "p-4 bg-neutral-800 rounded mb-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider", children: "CURRENT LEVEL" }), (0, jsx_runtime_1.jsx)("div", { className: "text-lg font-bold text-cyan-300", children: levelLabel })] }), (0, jsx_runtime_1.jsxs)("span", { className: "px-3 py-1 bg-cyan-900/30 text-cyan-300 rounded border border-cyan-700 text-xs", children: ["Lv.", Object.keys(LEVEL_LABELS).indexOf(level) + 3] })] }) }), userQueue?.is_onboarded ? ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-700", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "Onboarded" }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-neutral-400", children: ["Assigned to ", userQueue.station_name || userQueue.territory_id || ''] })] }), (0, jsx_runtime_1.jsx)("span", { className: "px-2 py-1 bg-white/20 text-white rounded text-xs", children: "Active" })] }), (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-1 gap-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 rounded-lg p-4 text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl font-bold text-white", children: userQueue.territory_id }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Territory ID" })] }) })] })) : userQueue?.in_queue ? ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-800", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "In Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Waiting for station assignment" })] }), userQueue.status && ((0, jsx_runtime_1.jsx)("span", { className: "px-2 py-1 bg-cyan-900/30 text-cyan-300 rounded text-xs", children: userQueue.status }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: userQueue.position || '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Position" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: userQueue.estimated_wait_minutes || '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Est. wait (min)" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-white truncate", children: userQueue.preferred_territory_id || 'Auto' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Preferred" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-white", children: userQueue.joined_at ? new Date(userQueue.joined_at).toLocaleString() : '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Joined at" })] })] }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setShowCancelConfirm(true), disabled: submitting, className: "w-full py-2 bg-neutral-800 hover:bg-neutral-700 text-white rounded transition-colors disabled:opacity-50", children: "Cancel Queue" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-800", children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "Not Onboarded" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Join the queue to be assigned a station" })] }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setShowJoinConfirm(true), disabled: submitting, className: "w-full py-2 bg-white/20 hover:bg-neutral-800 text-white border border-neutral-700 rounded transition-colors disabled:opacity-50", children: "Join Queue" })] }))] }), showJoinConfirm && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setShowJoinConfirm(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-md p-6 space-y-4", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-white font-medium", children: "Confirm Join Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "You will be added to the station queue. Auto-assignment happens when a slot opens." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setShowJoinConfirm(false), className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Cancel" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleJoin, disabled: submitting, className: "flex-1 px-3 py-2 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Joining...' : 'Confirm' })] })] }) })), showCancelConfirm && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setShowCancelConfirm(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-md p-6 space-y-4", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-white font-medium", children: "Cancel Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Are you sure? You will lose your current queue position." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setShowCancelConfirm(false), className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Keep Waiting" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCancel, disabled: submitting, className: "flex-1 px-3 py-2 bg-red-700 hover:bg-red-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Cancelling...' : 'Confirm Cancel' })] })] }) }))] }));
112
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "max-w-7xl mx-auto px-3 py-4 space-y-3 xs:px-4 xs:space-y-4 sm:px-6 sm:py-6 sm:space-y-6 font-mono", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-lg sm:text-2xl font-bold text-white tracking-wider", children: "Station Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs sm:text-sm text-neutral-400", children: "Your onboarding status" })] }), (0, jsx_runtime_1.jsx)("button", { onClick: loadStatus, className: "px-3 py-1.5 text-xs border border-neutral-700 text-neutral-400 hover:bg-neutral-800 hover:text-white rounded transition-colors", children: "Refresh" })] }), error && ((0, jsx_runtime_1.jsx)("div", { className: "p-3 bg-red-900/20 border border-red-800 rounded text-sm text-red-300", children: error })), (0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "p-4 bg-neutral-800 rounded mb-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider", children: "CURRENT LEVEL" }), (0, jsx_runtime_1.jsx)("div", { className: "text-lg font-bold text-cyan-300", children: levelLabel })] }), (0, jsx_runtime_1.jsxs)("span", { className: "px-3 py-1 bg-cyan-900/30 text-cyan-300 rounded border border-cyan-700 text-xs", children: ["Lv.", Object.keys(LEVEL_LABELS).indexOf(level) + 3] })] }) }), userQueue?.is_onboarded ? ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-700", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "Onboarded" }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-neutral-400", children: ["Assigned to ", userQueue.station_name || userQueue.territory_id || ''] })] }), (0, jsx_runtime_1.jsx)("span", { className: "px-2 py-1 bg-white/20 text-white rounded text-xs", children: "Active" })] }), (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-1 gap-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 rounded-lg p-4 text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl font-bold text-white", children: userQueue.territory_id }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Territory ID" })] }) })] })) : userQueue?.in_queue ? ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-800", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "In Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Waiting for station assignment" })] }), userQueue.status && ((0, jsx_runtime_1.jsx)("span", { className: "px-2 py-1 bg-cyan-900/30 text-cyan-300 rounded text-xs", children: userQueue.status }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: userQueue.position || '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Position" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: userQueue.estimated_wait_minutes || '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Est. wait (min)" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-white truncate", children: userQueue.preferred_territory_id || 'Auto' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Preferred" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-white", children: userQueue.joined_at ? new Date(userQueue.joined_at).toLocaleString() : '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400", children: "Joined at" })] })] }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setShowCancelConfirm(true), disabled: submitting, className: "w-full py-2 bg-neutral-800 hover:bg-neutral-700 text-white rounded transition-colors disabled:opacity-50", children: "Cancel Queue" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded-lg p-6 border border-neutral-800", children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-white", children: "Not Onboarded" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Join the queue to be assigned a station" })] }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setShowJoinConfirm(true), disabled: submitting, className: "w-full py-2 bg-white/20 hover:bg-neutral-800 text-white border border-neutral-700 rounded transition-colors disabled:opacity-50", children: "Join Queue" })] }))] }), isAdmin && adminQueue && ((0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { title: "Queue Management (Admin)", children: [(0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded p-4 text-center border border-neutral-800", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: adminQueue.pending_count ?? 0 }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400 mt-1", children: "Pending" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded p-4 text-center border border-neutral-800", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: adminQueue.processing_count ?? 0 }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400 mt-1", children: "Processing" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded p-4 text-center border border-neutral-800", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold text-white", children: adminQueue.total_in_queue ?? 0 }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400 mt-1", children: "Total in queue" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-950 rounded p-4 text-center border border-neutral-800", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-white", children: adminQueue.oldest_pending ? new Date(adminQueue.oldest_pending).toLocaleString() : '-' }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-400 mt-1", children: "Oldest pending" })] })] }), (0, jsx_runtime_1.jsx)("button", { onClick: handleProcessQueue, disabled: submitting, className: "w-full px-4 py-2 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Processing...' : 'Process Queue (batch 50)' })] })), isAdmin && expansion && ((0, jsx_runtime_1.jsx)(terminal_1.TerminalCard, { title: "Station Expansion (Admin)", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-sm text-neutral-300", children: ["Needs expansion: ", expansion.needs_expansion ? 'YES' : 'NO'] }), expansion.reason && ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-neutral-500 mt-1", children: ["Reason: ", expansion.reason] }))] }), (0, jsx_runtime_1.jsx)("button", { onClick: handleTriggerExpansion, disabled: submitting || !expansion.needs_expansion, className: "px-4 py-2 bg-red-900/30 text-red-300 border border-red-800 hover:bg-red-900/50 rounded text-sm disabled:opacity-50", children: submitting ? 'Expanding...' : 'Trigger Expansion' })] }) })), showJoinConfirm && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setShowJoinConfirm(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-md p-6 space-y-4", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-white font-medium", children: "Confirm Join Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "You will be added to the station queue. Auto-assignment happens when a slot opens." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setShowJoinConfirm(false), className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Cancel" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleJoin, disabled: submitting, className: "flex-1 px-3 py-2 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Joining...' : 'Confirm' })] })] }) })), showCancelConfirm && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setShowCancelConfirm(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-md p-6 space-y-4", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-white font-medium", children: "Cancel Queue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-neutral-400", children: "Are you sure? You will lose your current queue position." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setShowCancelConfirm(false), className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Keep Waiting" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCancel, disabled: submitting, className: "flex-1 px-3 py-2 bg-red-700 hover:bg-red-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Cancelling...' : 'Confirm Cancel' })] })] }) }))] }));
73
113
  }
@@ -2,5 +2,6 @@ export interface MicrocosmStationListPageProps {
2
2
  basePath?: string;
3
3
  onNavigate?: (path: string) => void;
4
4
  currentUid?: string;
5
+ isAdmin?: boolean;
5
6
  }
6
- export declare function MicrocosmStationListPage({ currentUid }?: MicrocosmStationListPageProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function MicrocosmStationListPage({ currentUid, isAdmin }?: MicrocosmStationListPageProps): import("react/jsx-runtime").JSX.Element;
@@ -6,10 +6,40 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
7
  const auth_react_1 = require("@microcosmmoney/auth-react");
8
8
  const terminal_1 = require("../terminal");
9
+ const API_BASE = 'https://api.microcosm.money/v1';
10
+ const cropToSquare = (file, size) => {
11
+ return new Promise((resolve, reject) => {
12
+ const img = new Image();
13
+ img.onload = () => {
14
+ const canvas = document.createElement('canvas');
15
+ canvas.width = size;
16
+ canvas.height = size;
17
+ const ctx = canvas.getContext('2d');
18
+ if (!ctx) {
19
+ reject(new Error('Canvas not supported'));
20
+ return;
21
+ }
22
+ const side = Math.min(img.width, img.height);
23
+ const sx = (img.width - side) / 2;
24
+ const sy = (img.height - side) / 2;
25
+ ctx.drawImage(img, sx, sy, side, side, 0, 0, size, size);
26
+ canvas.toBlob((blob) => {
27
+ if (!blob) {
28
+ reject(new Error('Blob creation failed'));
29
+ return;
30
+ }
31
+ resolve(new File([blob], file.name, { type: 'image/jpeg' }));
32
+ }, 'image/jpeg', 0.85);
33
+ };
34
+ img.onerror = () => reject(new Error('Image load failed'));
35
+ img.src = URL.createObjectURL(file);
36
+ });
37
+ };
9
38
  const UNIT_LABELS = { station: 'Station', matrix: 'Matrix', sector: 'Sector', system: 'System' };
10
39
  const MAGISTRATE_TITLES = { station: 'Commander', matrix: 'Pioneer', sector: 'Warden', system: 'Admiral' };
11
- function MicrocosmStationListPage({ currentUid } = {}) {
40
+ function MicrocosmStationListPage({ currentUid, isAdmin = false } = {}) {
12
41
  const api = (0, auth_react_1.useMicrocosmApi)();
42
+ const { getAccessToken } = (0, auth_react_1.useMicrocosmContext)();
13
43
  const [units, setUnits] = (0, react_1.useState)([]);
14
44
  const [summary, setSummary] = (0, react_1.useState)(null);
15
45
  const [loading, setLoading] = (0, react_1.useState)(true);
@@ -20,6 +50,10 @@ function MicrocosmStationListPage({ currentUid } = {}) {
20
50
  const [editFormData, setEditFormData] = (0, react_1.useState)({ unit_name: '', description: '' });
21
51
  const [submitting, setSubmitting] = (0, react_1.useState)(false);
22
52
  const [error, setError] = (0, react_1.useState)(null);
53
+ const [imageFile, setImageFile] = (0, react_1.useState)(null);
54
+ const [imagePreview, setImagePreview] = (0, react_1.useState)(null);
55
+ const [uploadingImage, setUploadingImage] = (0, react_1.useState)(false);
56
+ const fileInputRef = (0, react_1.useRef)(null);
23
57
  const loadSummary = (0, react_1.useCallback)(async () => {
24
58
  try {
25
59
  const res = await api.get('/territories/summary');
@@ -75,17 +109,91 @@ function MicrocosmStationListPage({ currentUid } = {}) {
75
109
  const openEditDialog = (unit) => {
76
110
  setEditingUnit(unit);
77
111
  setEditFormData({ unit_name: unit.unit_name, description: unit.description || '' });
112
+ setImageFile(null);
113
+ setImagePreview(null);
114
+ };
115
+ const handleFileSelect = (e) => {
116
+ const file = e.target.files?.[0];
117
+ if (!file)
118
+ return;
119
+ if (!file.type.startsWith('image/')) {
120
+ setError('Please select an image file');
121
+ return;
122
+ }
123
+ if (file.size > 2 * 1024 * 1024) {
124
+ setError('Image must be under 2MB');
125
+ return;
126
+ }
127
+ cropToSquare(file, 512)
128
+ .then(cropped => {
129
+ setImageFile(cropped);
130
+ const reader = new FileReader();
131
+ reader.onload = (ev) => setImagePreview(ev.target?.result);
132
+ reader.readAsDataURL(cropped);
133
+ })
134
+ .catch(() => setError('Image processing failed'));
135
+ };
136
+ const uploadImage = async (unitId) => {
137
+ if (!imageFile)
138
+ return true;
139
+ setUploadingImage(true);
140
+ try {
141
+ const token = await getAccessToken();
142
+ if (!token)
143
+ throw new Error('Not authenticated');
144
+ const formData = new FormData();
145
+ formData.append('image', imageFile, imageFile.name);
146
+ const response = await fetch(`${API_BASE}/territories/${unitId}/image`, {
147
+ method: 'POST',
148
+ headers: { Authorization: `Bearer ${token}` },
149
+ body: formData,
150
+ });
151
+ if (!response.ok) {
152
+ const err = await response.json().catch(() => ({}));
153
+ throw new Error(err.detail || err.error || 'Image upload failed');
154
+ }
155
+ return true;
156
+ }
157
+ catch (e) {
158
+ setError(e instanceof Error ? e.message : 'Image upload failed');
159
+ return false;
160
+ }
161
+ finally {
162
+ setUploadingImage(false);
163
+ }
164
+ };
165
+ const handleImageReview = async (unitId, status) => {
166
+ try {
167
+ await api.put(`/territories/${unitId}/image/review`, { status });
168
+ await loadUnits();
169
+ if (editingUnit && editingUnit.unit_id === unitId) {
170
+ setEditingUnit({ ...editingUnit, image_status: status });
171
+ }
172
+ }
173
+ catch (e) {
174
+ setError(e instanceof Error ? e.message : 'Review failed');
175
+ }
78
176
  };
79
177
  const handleEdit = async () => {
80
178
  if (!editingUnit || !editFormData.unit_name.trim())
81
179
  return;
82
180
  setSubmitting(true);
181
+ setError(null);
83
182
  try {
183
+ if (imageFile) {
184
+ const ok = await uploadImage(editingUnit.unit_id);
185
+ if (!ok) {
186
+ setSubmitting(false);
187
+ return;
188
+ }
189
+ }
84
190
  await api.put(`/territories/${editingUnit.unit_id}`, {
85
191
  unit_name: editFormData.unit_name,
86
192
  description: editFormData.description,
87
193
  });
88
194
  setEditingUnit(null);
195
+ setImageFile(null);
196
+ setImagePreview(null);
89
197
  await loadUnits();
90
198
  }
91
199
  catch (e) {
@@ -98,5 +206,13 @@ function MicrocosmStationListPage({ currentUid } = {}) {
98
206
  return ((0, jsx_runtime_1.jsxs)("div", { className: "max-w-7xl mx-auto px-3 py-4 space-y-3 xs:px-4 xs:space-y-4 sm:px-6 sm:py-6 sm:space-y-6 font-mono", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-lg sm:text-2xl font-bold text-white tracking-wider", children: "Station List" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs sm:text-sm text-neutral-400", children: "All territories (Station / Matrix / Sector / System)" })] }), error && ((0, jsx_runtime_1.jsx)("div", { className: "p-3 bg-red-900/20 border border-red-800 rounded text-sm text-red-300", children: error })), summary && ((0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [(0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider mb-1", children: "TOTAL STATIONS" }), (0, jsx_runtime_1.jsx)("div", { className: "text-2xl font-bold text-white", children: summary.total_stations ?? 0 })] }), (0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider mb-1", children: "TOTAL MEMBERS" }), (0, jsx_runtime_1.jsx)("div", { className: "text-2xl font-bold text-cyan-400", children: summary.total_members ?? 0 })] }), (0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider mb-1", children: "VAULT MCD" }), (0, jsx_runtime_1.jsx)("div", { className: "text-2xl font-bold text-cyan-400", children: (summary.total_vault_mcd ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 }) })] }), (0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 tracking-wider mb-1", children: "AVG KPI" }), (0, jsx_runtime_1.jsx)("div", { className: "text-2xl font-bold text-white", children: (summary.avg_kpi_score ?? 0).toFixed(1) })] })] })), (0, jsx_runtime_1.jsx)(terminal_1.TerminalCard, { children: (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col md:flex-row gap-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex gap-2 flex-wrap", children: ['all', 'station', 'matrix', 'sector', 'system'].map(f => ((0, jsx_runtime_1.jsx)("button", { onClick: () => setFilter(f), className: `px-3 py-1.5 text-sm rounded transition-colors ${filter === f ? 'bg-cyan-700 text-white' : 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700 hover:text-white'}`, children: f === 'all' ? 'All' : UNIT_LABELS[f] }, f))) }), (0, jsx_runtime_1.jsx)("input", { type: "text", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), placeholder: "Search by name / location / short_id...", className: "flex-1 bg-neutral-800 border border-neutral-600 text-white rounded px-3 py-1.5 text-sm focus:outline-none focus:border-cyan-400" })] }) }), loading ? ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center py-20", children: (0, jsx_runtime_1.jsx)("span", { className: "text-neutral-400", children: "Loading units..." }) })) : filteredUnits.length === 0 ? ((0, jsx_runtime_1.jsx)(terminal_1.TerminalCard, { children: (0, jsx_runtime_1.jsx)("div", { className: "text-center py-8 text-neutral-500", children: "No units match your filter" }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4", children: filteredUnits.map(unit => {
99
207
  const metrics = unitDataCache[unit.unit_id];
100
208
  return ((0, jsx_runtime_1.jsxs)(terminal_1.TerminalCard, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-white font-semibold", children: unit.unit_name }), (0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-neutral-500", children: [UNIT_LABELS[unit.unit_type], " \u00B7 ", MAGISTRATE_TITLES[unit.unit_type]] }), unit.short_id && ((0, jsx_runtime_1.jsx)("div", { className: "text-xs font-mono text-cyan-400 mt-1", children: unit.short_id }))] }), canEditUnit(unit) && ((0, jsx_runtime_1.jsx)("button", { onClick: () => openEditDialog(unit), className: "text-xs px-2 py-1 border border-neutral-700 text-neutral-400 hover:text-white hover:bg-neutral-800 rounded", children: "Edit" }))] }), unit.description && ((0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-400 mb-3 line-clamp-2", children: unit.description })), metrics && ((0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 gap-2 pt-3 border-t border-neutral-700", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-500", children: "Members" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-sm text-white", children: [metrics.member_count, "/", metrics.max_capacity] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-500", children: "Occupancy" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-sm text-cyan-400", children: [(metrics.occupancy_rate * 100).toFixed(0), "%"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-span-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-500", children: "Vault MCD" }), (0, jsx_runtime_1.jsx)("div", { className: "text-sm text-white", children: (metrics.vault_mcd ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 }) })] })] })), unit.image_status === 'pending' && ((0, jsx_runtime_1.jsx)("div", { className: "mt-2 text-xs text-yellow-400", children: "Image pending review" }))] }, unit.unit_id));
101
- }) })), editingUnit && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setEditingUnit(null), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-lg p-6 space-y-4", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("h3", { className: "text-white font-medium", children: ["Edit ", editingUnit.unit_name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "text-xs text-neutral-400 tracking-wider block mb-1", children: "Name" }), (0, jsx_runtime_1.jsx)("input", { type: "text", value: editFormData.unit_name, onChange: (e) => setEditFormData({ ...editFormData, unit_name: e.target.value }), className: "w-full bg-neutral-800 border border-neutral-600 text-white rounded px-3 py-2 text-sm" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "text-xs text-neutral-400 tracking-wider block mb-1", children: "Description" }), (0, jsx_runtime_1.jsx)("textarea", { value: editFormData.description, onChange: (e) => setEditFormData({ ...editFormData, description: e.target.value }), rows: 4, className: "w-full bg-neutral-800 border border-neutral-600 text-white rounded px-3 py-2 text-sm" })] }), (0, jsx_runtime_1.jsx)("div", { className: "text-xs text-neutral-500", children: "Note: image upload available on main portal only" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setEditingUnit(null), className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Cancel" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleEdit, disabled: submitting || !editFormData.unit_name.trim(), className: "flex-1 px-3 py-2 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm disabled:opacity-50", children: submitting ? 'Saving...' : 'Save' })] })] }) }))] }));
209
+ }) })), editingUnit && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", onClick: () => setEditingUnit(null), children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-neutral-900 border border-neutral-700 rounded-lg w-full max-w-lg p-6 space-y-4 max-h-[90vh] overflow-y-auto", onClick: e => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("h3", { className: "text-white font-medium", children: ["Edit ", editingUnit.unit_name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "text-xs text-neutral-400 tracking-wider block mb-1", children: "Name" }), (0, jsx_runtime_1.jsx)("input", { type: "text", value: editFormData.unit_name, onChange: (e) => setEditFormData({ ...editFormData, unit_name: e.target.value }), className: "w-full bg-neutral-800 border border-neutral-600 text-white rounded px-3 py-2 text-sm" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "text-xs text-neutral-400 tracking-wider block mb-1", children: "Description" }), (0, jsx_runtime_1.jsx)("textarea", { value: editFormData.description, onChange: (e) => setEditFormData({ ...editFormData, description: e.target.value }), rows: 4, className: "w-full bg-neutral-800 border border-neutral-600 text-white rounded px-3 py-2 text-sm" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "text-xs text-neutral-400 tracking-wider block mb-1", children: "Image" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start gap-3", children: [(imagePreview || editingUnit.image_url) ? ((0, jsx_runtime_1.jsx)("img", { src: imagePreview || editingUnit.image_url, alt: "", className: "w-24 h-24 rounded object-cover border border-neutral-700" })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-24 h-24 rounded bg-neutral-800 border border-neutral-700 flex items-center justify-center text-xs text-neutral-500", children: "No image" })), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-2", children: [(0, jsx_runtime_1.jsx)("input", { ref: fileInputRef, type: "file", accept: "image/jpeg,image/png,image/webp", onChange: handleFileSelect, className: "hidden" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => fileInputRef.current?.click(), disabled: uploadingImage, className: "w-full px-3 py-1.5 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 hover:text-white rounded text-sm", children: imageFile ? 'Change' : 'Select image' }), imageFile && ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-neutral-500", children: [imageFile.name, " (", (imageFile.size / 1024).toFixed(1), "KB)"] })), editingUnit.image_status && ((0, jsx_runtime_1.jsx)("div", { className: `text-xs px-2 py-0.5 rounded inline-block ${editingUnit.image_status === 'approved'
210
+ ? 'bg-green-900/30 text-green-400'
211
+ : editingUnit.image_status === 'rejected'
212
+ ? 'bg-red-900/30 text-red-400'
213
+ : 'bg-yellow-900/30 text-yellow-400'}`, children: editingUnit.image_status }))] })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-neutral-500 mt-2", children: "Max 2MB. Auto-cropped to square (512\u00D7512). JPG/PNG/WebP." })] }), isAdmin && editingUnit.image_url && editingUnit.image_status === 'pending' && ((0, jsx_runtime_1.jsxs)("div", { className: "pt-3 border-t border-neutral-700 space-y-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-xs text-cyan-400 tracking-wider", children: "ADMIN REVIEW" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => handleImageReview(editingUnit.unit_id, 'approved'), className: "flex-1 px-3 py-1.5 bg-green-900/30 text-green-400 hover:bg-green-900/50 border border-green-800 rounded text-sm", children: "Approve" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => handleImageReview(editingUnit.unit_id, 'rejected'), className: "flex-1 px-3 py-1.5 bg-red-900/30 text-red-400 hover:bg-red-900/50 border border-red-800 rounded text-sm", children: "Reject" })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 pt-2", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => {
214
+ setEditingUnit(null);
215
+ setImageFile(null);
216
+ setImagePreview(null);
217
+ }, className: "flex-1 px-3 py-2 border border-neutral-700 text-neutral-400 hover:bg-neutral-800 rounded text-sm", children: "Cancel" }), (0, jsx_runtime_1.jsx)("button", { onClick: handleEdit, disabled: submitting || uploadingImage || !editFormData.unit_name.trim(), className: "flex-1 px-3 py-2 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm disabled:opacity-50", children: uploadingImage ? 'Uploading...' : submitting ? 'Saving...' : 'Save' })] })] }) }))] }));
102
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microcosmmoney/portal-react",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Microcosm Portal UI components for React/Next.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",