Add app category grouping support

Add category column to apps table with migration, update seed with category
values for existing apps, and include category in auth queries ordered by
category then name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-01 18:26:01 +00:00
parent a0edbec92b
commit 7fd1ed8381
2 changed files with 17 additions and 13 deletions

View file

@ -23,8 +23,9 @@ export async function initDb() {
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
base_path TEXT NOT NULL, base_path TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '📋', icon TEXT NOT NULL DEFAULT 'ClipboardList',
theme_color TEXT NOT NULL DEFAULT '#1e3a5f', theme_color TEXT NOT NULL DEFAULT '#1e3a5f',
category VARCHAR(100),
active BOOLEAN NOT NULL DEFAULT TRUE, active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
@ -37,17 +38,20 @@ export async function initDb() {
); );
`) `)
// Add category column to existing installs
await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS category VARCHAR(100)`)
// Seed built-in apps // Seed built-in apps
await pool.query(` await pool.query(`
INSERT INTO apps (slug, name, description, base_path, icon, theme_color) INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category)
VALUES VALUES
('noticeboard', 'Noticeboard', 'Staff notices and announcements', '/notices', 'ClipboardList', '#1e3a5f'), ('noticeboard', 'Noticeboard', 'Staff notices and announcements', '/notices', 'ClipboardList', '#1e3a5f', NULL),
('kitchen', 'Kitchen Flash','Invoice processing and GP tracking', '/kitchen', 'ChefHat', '#e85d04'), ('kitchen', 'Kitchen Flash','Invoice processing and GP tracking', '/kitchen', 'ChefHat', '#e85d04', 'Kitchen'),
('cashup', 'Cash Up', 'Hotel daily cashing up', '/cashup', 'Banknote', '#6b2d8b'), ('cashup', 'Cash Up', 'Hotel daily cashing up', '/cashup', 'Banknote', '#6b2d8b', 'Finance'),
('housekeeping','Housekeeping', 'Room status and task management', '/hk', 'BedDouble', '#2d6a4f'), ('housekeeping','Housekeeping', 'Room status and task management', '/hk', 'BedDouble', '#2d6a4f', 'Housekeeping'),
('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecast','TrendingUp', '#0077b6'), ('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecast','TrendingUp', '#0077b6', 'Finance'),
('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00') ('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00', 'Finance')
ON CONFLICT (slug) DO UPDATE SET icon = EXCLUDED.icon ON CONFLICT (slug) DO UPDATE SET icon = EXCLUDED.icon, category = EXCLUDED.category
`) `)
// Seed first admin user if table is empty // Seed first admin user if table is empty

View file

@ -32,17 +32,17 @@ async function getUserWithApps(userId) {
// only the apps explicitly granted to them via user_app_perms. // only the apps explicitly granted to them via user_app_perms.
const { rows: apps } = user.is_admin const { rows: apps } = user.is_admin
? await pool.query( ? await pool.query(
`SELECT a.slug, a.name, a.description, a.base_path, a.icon, a.theme_color `SELECT a.slug, a.name, a.description, a.base_path, a.icon, a.theme_color, a.category
FROM apps a FROM apps a
WHERE a.active = true WHERE a.active = true
ORDER BY a.name` ORDER BY a.category NULLS FIRST, a.name`
) )
: await pool.query( : await pool.query(
`SELECT a.slug, a.name, a.description, a.base_path, a.icon, a.theme_color `SELECT a.slug, a.name, a.description, a.base_path, a.icon, a.theme_color, a.category
FROM apps a FROM apps a
JOIN user_app_perms p ON p.app_id = a.id JOIN user_app_perms p ON p.app_id = a.id
WHERE p.user_id = $1 AND a.active = true WHERE p.user_id = $1 AND a.active = true
ORDER BY a.name`, ORDER BY a.category NULLS FIRST, a.name`,
[userId] [userId]
) )