Task #5: Refine home and services screens
- Removed greeting and welcome banner from the home page; trimmed unused ar.json/en.json greeting keys. - Hid the 4-tile stats row for non-admin users (`isAdmin` from user roles) and scoped `totalApps` in /api/stats/home to apps the user can actually access (mirrors the RBAC join in /api/apps). - Renamed nav.services translation: "خدماتي" -> "الخدمات", "My Services" -> "Services". - Removed price/free text from the user-facing services page; cards now show name, description, availability badge, and (when present) image. - Admin service editor now exposes an "Image URL" / "رابط الصورة" text field with a live preview thumbnail; saved imageUrl renders at the top of the corresponding service card (16:10, object-cover, onError hides broken images). Uses existing imageUrl column in services schema; no DB migration required. Image upload via object storage was deferred — see follow-up task. Verified: typecheck passes for teaboy-os and api-server; api-server restarted clean; e2e plan validated by testing subagent; architect code review approved.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { eq, and, inArray, sql } from "drizzle-orm";
|
||||
import { db } from "@workspace/db";
|
||||
import {
|
||||
appsTable,
|
||||
appPermissionsTable,
|
||||
rolesTable,
|
||||
rolePermissionsTable,
|
||||
userRolesTable,
|
||||
servicesTable,
|
||||
notificationsTable,
|
||||
usersTable,
|
||||
@@ -16,10 +20,57 @@ const router: IRouter = Router();
|
||||
router.get("/stats/home", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
|
||||
const [appsCount] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true));
|
||||
// Determine if the user is an admin and which apps they can access.
|
||||
const userRoleRows = await db
|
||||
.select({ roleName: rolesTable.name, roleId: userRolesTable.roleId })
|
||||
.from(userRolesTable)
|
||||
.innerJoin(rolesTable, eq(userRolesTable.roleId, rolesTable.id))
|
||||
.where(eq(userRolesTable.userId, userId));
|
||||
|
||||
const isAdmin = userRoleRows.some((r) => r.roleName === "admin");
|
||||
|
||||
let totalApps = 0;
|
||||
if (isAdmin) {
|
||||
const [row] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true));
|
||||
totalApps = Number(row?.count ?? 0);
|
||||
} else {
|
||||
const userRoleIds = userRoleRows.map((r) => r.roleId);
|
||||
const userPermissionIds = userRoleIds.length > 0
|
||||
? (await db
|
||||
.select({ permissionId: rolePermissionsTable.permissionId })
|
||||
.from(rolePermissionsTable)
|
||||
.where(inArray(rolePermissionsTable.roleId, userRoleIds))
|
||||
).map((r) => r.permissionId)
|
||||
: [];
|
||||
|
||||
const restrictedRows = await db
|
||||
.select({ appId: appPermissionsTable.appId })
|
||||
.from(appPermissionsTable);
|
||||
const restrictedAppIds = new Set(restrictedRows.map((r) => r.appId));
|
||||
|
||||
const allowedRestrictedAppIds = userPermissionIds.length > 0
|
||||
? new Set(
|
||||
(await db
|
||||
.select({ appId: appPermissionsTable.appId })
|
||||
.from(appPermissionsTable)
|
||||
.where(inArray(appPermissionsTable.permissionId, userPermissionIds))
|
||||
).map((r) => r.appId),
|
||||
)
|
||||
: new Set<number>();
|
||||
|
||||
const activeApps = await db
|
||||
.select({ id: appsTable.id })
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true));
|
||||
totalApps = activeApps.filter(
|
||||
(a) => !restrictedAppIds.has(a.id) || allowedRestrictedAppIds.has(a.id),
|
||||
).length;
|
||||
}
|
||||
|
||||
const appsCount = { count: totalApps };
|
||||
|
||||
const [servicesCount] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
|
||||
Reference in New Issue
Block a user