diff --git a/frontend/js/components/workspace-switcher.js b/frontend/js/components/workspace-switcher.js index 968eefd..1c8d213 100644 --- a/frontend/js/components/workspace-switcher.js +++ b/frontend/js/components/workspace-switcher.js @@ -1,5 +1,18 @@ import { api } from '../api.js'; import { showToast } from './toast.js'; +import { t, tn } from '../i18n.js'; + +// Reusable resource-count formatter. Returns localized "1 device" / "N devices" +// / "No devices" based on n. Generic so the same shape can wire users / +// playlists / schedules counts later without refactor - caller supplies the +// i18n key bases. +// keyBase: e.g. 'switcher.devices_count' (looks up _one / _other variants via tn) +// zeroKey: e.g. 'switcher.no_devices' (direct lookup for n === 0) +function formatResourceCount(n, keyBase, zeroKey) { + if (n === undefined || n === null) return ''; + if (n === 0) return t(zeroKey); + return tn(keyBase, n); +} // Render the workspace switcher inside #workspaceSwitcher based on the // /api/auth/me response. Three modes: @@ -37,14 +50,21 @@ export function renderWorkspaceSwitcher(me) {
- ${sorted.map(w => ` + ${sorted.map(w => { + const countStr = formatResourceCount(w.device_count, 'switcher.devices_count', 'switcher.no_devices'); + const orgName = w.organization_name || ''; + const subtitle = orgName && countStr ? esc(orgName) + ' ยท ' + esc(countStr) + : orgName ? esc(orgName) + : countStr ? esc(countStr) + : ''; + return `
${esc(w.name)}
-
${esc(w.organization_name || '')}
+
${subtitle}
${w.can_admin ? ` ` : ''}
- `).join('')} + `; + }).join('')}
`; diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index f5633f2..24d7a1d 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -1105,4 +1105,10 @@ export default { 'add_display.windows': 'Windows', 'add_display.smart_tv_note': 'Smart TVs (LG/Samsung): open the built-in browser and navigate to /player', 'add_display.pair_btn': 'Pair Display', + + // Workspace switcher (Phase 3 MVP). devices_count is the only count exposed + // today; matching pattern for users/playlists/etc. when those land later. + 'switcher.devices_count_one': '1 device', + 'switcher.devices_count_other': '{n} devices', + 'switcher.no_devices': 'No devices', }; diff --git a/server/routes/auth.js b/server/routes/auth.js index ce04777..c0db230 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -305,11 +305,16 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => { // UI can render admin affordances (rename pencil etc.) only where the // caller has permission. The server still enforces permission on the // actual mutation routes regardless of this advisory flag. + // device_count: correlated subquery on workspaces.id. Equality fails on NULL + // so unclaimed pair-pool devices (workspace_id IS NULL) are correctly excluded. + // Microseconds per row at current scale (~37 rows worst case for platform_admin); + // not optimizing - revisit if the admin list grows past a few hundred workspaces. const isPlatformAdmin = req.user.role === 'platform_admin' || req.user.role === 'superadmin'; const accessible = isPlatformAdmin ? db.prepare(` SELECT w.id, w.name, w.organization_id, o.name AS organization_name, - wm.role AS workspace_role, om.role AS org_role + wm.role AS workspace_role, om.role AS org_role, + (SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count FROM workspaces w JOIN organizations o ON o.id = w.organization_id LEFT JOIN workspace_members wm ON wm.workspace_id = w.id AND wm.user_id = ? @@ -318,7 +323,8 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => { `).all(req.user.id, req.user.id) : db.prepare(` SELECT w.id, w.name, w.organization_id, o.name AS organization_name, - wm.role AS workspace_role, om.role AS org_role + wm.role AS workspace_role, om.role AS org_role, + (SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count FROM workspace_members wm JOIN workspaces w ON w.id = wm.workspace_id JOIN organizations o ON o.id = w.organization_id