diff --git a/.replit b/.replit index c3ff4566..7543f223 100644 --- a/.replit +++ b/.replit @@ -27,9 +27,17 @@ externalPort = 8080 localPort = 8081 externalPort = 80 +[[ports]] +localPort = 8082 +externalPort = 3003 + [[ports]] localPort = 25785 externalPort = 3000 +[[ports]] +localPort = 25786 +externalPort = 3002 + [nix] channel = "stable-25_05" diff --git a/artifacts/api-server/src/routes/stats.ts b/artifacts/api-server/src/routes/stats.ts index 53adc096..13bc9bd6 100644 --- a/artifacts/api-server/src/routes/stats.ts +++ b/artifacts/api-server/src/routes/stats.ts @@ -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 => { const userId = req.session.userId!; - const [appsCount] = await db - .select({ count: sql`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`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(); + + 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`count(*)` }) diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 76c648e8..bc59c863 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -1,7 +1,7 @@ { "nav": { "home": "الرئيسية", - "services": "خدماتي", + "services": "الخدمات", "chat": "المحادثات", "notifications": "الإشعارات", "admin": "لوحة التحكم", @@ -21,11 +21,7 @@ "displayNameEn": "الاسم (إنجليزي)" }, "home": { - "goodMorning": "صباح الخير", - "goodAfternoon": "مساء الخير", - "goodEvening": "مساء الخير", - "myApps": "تطبيقاتي", - "welcome": "مرحباً بك في نظام TeaBoy" + "myApps": "تطبيقاتي" }, "services": { "title": "الخدمات", @@ -83,6 +79,7 @@ "serviceDescriptionEn": "الوصف (إنجليزي)", "servicePrice": "السعر", "serviceAvailable": "متاح؟", + "serviceImageUrl": "رابط الصورة", "users": "المستخدمين", "username": "اسم المستخدم", "email": "البريد الإلكتروني", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index a5b57e00..e89892db 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -1,7 +1,7 @@ { "nav": { "home": "Home", - "services": "My Services", + "services": "Services", "chat": "Chat", "notifications": "Notifications", "admin": "Admin Dashboard", @@ -21,11 +21,7 @@ "displayNameEn": "Name (English)" }, "home": { - "goodMorning": "Good Morning", - "goodAfternoon": "Good Afternoon", - "goodEvening": "Good Evening", - "myApps": "My Apps", - "welcome": "Welcome to TeaBoy OS" + "myApps": "My Apps" }, "services": { "title": "Services", @@ -83,6 +79,7 @@ "serviceDescriptionEn": "Description (English)", "servicePrice": "Price", "serviceAvailable": "Available?", + "serviceImageUrl": "Image URL", "users": "Users", "username": "Username", "email": "Email", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index b1be35f2..d281a9e1 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -44,6 +44,7 @@ type ServiceForm = { descriptionAr: string; descriptionEn: string; price: string; + imageUrl: string; isAvailable: boolean; }; @@ -84,7 +85,7 @@ export default function AdminPage() { const [newUserForm, setNewUserForm] = useState<{ username: string; email: string; password: string } | null>(null); const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0 }; - const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", isAvailable: true }; + const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", imageUrl: "", isAvailable: true }; const handleSaveApp = () => { if (!editingApp) return; @@ -220,6 +221,7 @@ export default function AdminPage() { { field: "descriptionAr", label: t("admin.serviceDescriptionAr") }, { field: "descriptionEn", label: t("admin.serviceDescriptionEn") }, { field: "price", label: t("admin.servicePrice") }, + { field: "imageUrl", label: t("admin.serviceImageUrl") }, ] as { field: keyof ServiceForm; label: string }[] ).map(({ field, label }) => (
@@ -228,9 +230,20 @@ export default function AdminPage() { value={String(editingService.form[field])} onChange={(e) => setEditingService({ ...editingService, form: { ...editingService.form, [field]: e.target.value } })} className="bg-white/70 border-slate-200" + placeholder={field === "imageUrl" ? "https://..." : undefined} />
))} + {editingService.form.imageUrl && ( +
+ { (e.currentTarget as HTMLImageElement).style.display = "none"; }} + /> +
+ )}
{ logout.mutate(undefined, { @@ -144,14 +143,8 @@ export default function HomePage() { {/* Main Content */}
- {/* Greeting */} -
-

{greeting}

-

{t("home.welcome")}

-
- - {/* Stats Row */} - {stats && ( + {/* Stats Row — admin only */} + {isAdmin && stats && (
{stats.totalApps}
diff --git a/artifacts/teaboy-os/src/pages/services.tsx b/artifacts/teaboy-os/src/pages/services.tsx index 758dfb11..4fb386de 100644 --- a/artifacts/teaboy-os/src/pages/services.tsx +++ b/artifacts/teaboy-os/src/pages/services.tsx @@ -49,29 +49,36 @@ export default function ServicesPage() { {services.map((service) => { const name = lang === "ar" ? service.nameAr : service.nameEn; const description = lang === "ar" ? service.descriptionAr : service.descriptionEn; - const price = service.price ? parseFloat(service.price) : 0; return (
-
-

{name}

- - {service.isAvailable ? t("services.available") : t("services.unavailable")} - -
- - {description && ( -

{description}

+ {service.imageUrl && ( +
+ {name} { (e.currentTarget as HTMLImageElement).style.display = "none"; }} + /> +
)} +
+
+

{name}

+ + {service.isAvailable ? t("services.available") : t("services.unavailable")} + +
-
- {price === 0 ? t("services.free") : `${price} ${t("services.price")}`} + {description && ( +

{description}

+ )}
);