diff --git a/inventory-server/auth/routes.js b/inventory-server/auth/routes.js index ecec280..ef6636d 100644 --- a/inventory-server/auth/routes.js +++ b/inventory-server/auth/routes.js @@ -229,11 +229,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re const hashedPassword = await bcrypt.hash(password, saltRounds); // Insert new user + // Convert rocket_chat_user_id to integer if provided + const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null; + const userResult = await client.query(` INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING id - `, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rocket_chat_user_id || null]); + `, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rcUserId]); const userId = userResult.rows[0].id; @@ -360,7 +363,9 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r if (rocket_chat_user_id !== undefined) { updateFields.push(`rocket_chat_user_id = $${paramIndex++}`); - updateValues.push(rocket_chat_user_id || null); + // Convert to integer if not null/undefined, otherwise null + const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null; + updateValues.push(rcUserId); } // Update password if provided diff --git a/inventory-server/auth/server.js b/inventory-server/auth/server.js index d2c048b..46c00d2 100644 --- a/inventory-server/auth/server.js +++ b/inventory-server/auth/server.js @@ -62,6 +62,12 @@ app.post('/login', async (req, res) => { return res.status(403).json({ error: 'Account is inactive' }); } + // Update last login timestamp + await pool.query( + 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1', + [user.id] + ); + // Generate JWT token const token = jwt.sign( { userId: user.id, username: user.username }, @@ -76,7 +82,7 @@ app.post('/login', async (req, res) => { JOIN user_permissions up ON p.id = up.permission_id WHERE up.user_id = $1 `, [user.id]); - + const permissions = permissionsResult.rows.map(row => row.code); res.json({ diff --git a/inventory/src/components/auth/PERMISSIONS.md b/inventory/src/components/auth/PERMISSIONS.md index d99b4fa..b6f475c 100644 --- a/inventory/src/components/auth/PERMISSIONS.md +++ b/inventory/src/components/auth/PERMISSIONS.md @@ -154,6 +154,23 @@ Admin users automatically have all permissions. | `settings:templates` | Access to Template Management | | `settings:user_management` | Access to User Management | +### Dashboard Component Permissions +| Code | Description | +|------|-------------| +| `dashboard:stats` | Can view statistics cards on the Dashboard | +| `dashboard:realtime` | Can view realtime analytics on the Dashboard | +| `dashboard:financial` | Can view financial overview on the Dashboard | +| `dashboard:feed` | Can view event feed on the Dashboard | +| `dashboard:sales` | Can view sales chart on the Dashboard | +| `dashboard:products` | Can view product grid on the Dashboard | +| `dashboard:campaigns` | Can view Klaviyo campaigns on the Dashboard | +| `dashboard:analytics` | Can view analytics overview on the Dashboard | +| `dashboard:user_behavior` | Can view user behavior insights on the Dashboard | +| `dashboard:meta_campaigns` | Can view Meta campaigns on the Dashboard | +| `dashboard:typeform` | Can view Typeform metrics on the Dashboard | +| `dashboard:gorgias` | Can view Gorgias overview on the Dashboard | +| `dashboard:calls` | Can view Aircall metrics on the Dashboard | + ### Admin Permissions | Code | Description | |------|-------------| diff --git a/inventory/src/components/dashboard/Navigation.jsx b/inventory/src/components/dashboard/Navigation.jsx index 57e1215..6ea25ed 100644 --- a/inventory/src/components/dashboard/Navigation.jsx +++ b/inventory/src/components/dashboard/Navigation.jsx @@ -1,35 +1,51 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useContext, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { useScroll } from "@/contexts/DashboardScrollContext"; import { ArrowUpToLine } from "lucide-react"; +import { AuthContext } from "@/contexts/AuthContext"; const Navigation = () => { const [activeSections, setActiveSections] = useState([]); const { isStuck, scrollContainerRef, scrollToSection } = useScroll(); + const { user } = useContext(AuthContext); const buttonRefs = useRef({}); const scrollContainerRef2 = useRef(null); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const lastScrollLeft = useRef(0); const lastScrollTop = useRef(0); - // Define base sections that are always visible - const baseSections = [ - { id: "stats", label: "Statistics" }, - { id: "realtime", label: "Realtime" }, - { id: "feed", label: "Event Feed" }, - { id: "sales", label: "Sales Chart" }, - { id: "products", label: "Top Products" }, - { id: "campaigns", label: "Campaigns" }, - { id: "analytics", label: "Analytics" }, - { id: "user-behavior", label: "User Behavior" }, - { id: "meta-campaigns", label: "Meta Ads" }, - { id: "typeform", label: "Customer Surveys" }, - { id: "gorgias-overview", label: "Customer Service" }, - { id: "calls", label: "Calls" }, + // Define all possible sections with their permission requirements + const allSections = [ + { id: "stats", label: "Statistics", permission: "dashboard:stats" }, + { id: "realtime", label: "Realtime", permission: "dashboard:realtime" }, + { id: "financial", label: "Financial", permission: "dashboard:financial" }, + { id: "feed", label: "Event Feed", permission: "dashboard:feed" }, + { id: "sales", label: "Sales Chart", permission: "dashboard:sales" }, + { id: "products", label: "Top Products", permission: "dashboard:products" }, + { id: "campaigns", label: "Campaigns", permission: "dashboard:campaigns" }, + { id: "analytics", label: "Analytics", permission: "dashboard:analytics" }, + { id: "user-behavior", label: "User Behavior", permission: "dashboard:user_behavior" }, + { id: "meta-campaigns", label: "Meta Ads", permission: "dashboard:meta_campaigns" }, + { id: "typeform", label: "Customer Surveys", permission: "dashboard:typeform" }, + { id: "gorgias-overview", label: "Customer Service", permission: "dashboard:gorgias" }, + { id: "calls", label: "Calls", permission: "dashboard:calls" }, ]; + // Filter sections based on user permissions + const baseSections = useMemo(() => { + if (!user) return []; + + // Admins see all sections + if (user.is_admin) return allSections; + + // Filter sections based on user permissions + return allSections.filter(section => + user.permissions && user.permissions.includes(section.permission) + ); + }, [user]); + const sortSections = (sections) => { const isMediumScreen = window.matchMedia( "(min-width: 768px) and (max-width: 1023px)" diff --git a/inventory/src/components/settings/UserForm.tsx b/inventory/src/components/settings/UserForm.tsx index c3e35aa..4e1de0c 100644 --- a/inventory/src/components/settings/UserForm.tsx +++ b/inventory/src/components/settings/UserForm.tsx @@ -43,7 +43,7 @@ interface User { email?: string; is_admin: boolean; is_active: boolean; - rocket_chat_user_id?: string; + rocket_chat_user_id?: number; permissions?: Permission[]; } @@ -94,7 +94,7 @@ interface UserSaveData { password?: string; is_admin: boolean; is_active: boolean; - rocket_chat_user_id?: string; + rocket_chat_user_id?: number; permissions: Permission[]; } @@ -113,7 +113,8 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) password: "", // Don't pre-fill password is_admin: user?.is_admin || false, is_active: user?.is_active !== false, - rocket_chat_user_id: user?.rocket_chat_user_id || "none", + // Convert number to string for Select, default to "none" + rocket_chat_user_id: user?.rocket_chat_user_id ? user.rocket_chat_user_id.toString() : "none", }, }); @@ -165,7 +166,8 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) password: "", // Don't pre-fill password is_admin: user.is_admin || false, is_active: user.is_active !== false, - rocket_chat_user_id: user.rocket_chat_user_id || "none", + // Convert number to string for the Select component, or use "none" as default + rocket_chat_user_id: user.rocket_chat_user_id ? user.rocket_chat_user_id.toString() : "none", }); } else { // For new users, ensure rocket_chat_user_id defaults to "none" @@ -196,7 +198,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) const userData: UserSaveData = { ...data, id: user?.id, // Include ID if editing existing user - rocket_chat_user_id: data.is_admin ? undefined : (data.rocket_chat_user_id === "none" ? undefined : data.rocket_chat_user_id), + rocket_chat_user_id: data.is_admin ? undefined : (data.rocket_chat_user_id === "none" || !data.rocket_chat_user_id ? undefined : parseInt(data.rocket_chat_user_id, 10)), permissions: [] // Initialize with empty array }; diff --git a/inventory/src/pages/Dashboard.tsx b/inventory/src/pages/Dashboard.tsx index 158b7b6..56be0fe 100644 --- a/inventory/src/pages/Dashboard.tsx +++ b/inventory/src/pages/Dashboard.tsx @@ -1,5 +1,6 @@ import { ScrollProvider } from "@/contexts/DashboardScrollContext"; import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider"; +import { Protected } from "@/components/auth/Protected"; import AircallDashboard from "@/components/dashboard/AircallDashboard"; import EventFeed from "@/components/dashboard/EventFeed"; import StatCards from "@/components/dashboard/StatCards"; @@ -30,60 +31,86 @@ export function Dashboard() {
-
-
-
- + +
+
+
+ +
-
-
-
- + + +
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+ +
+ +
+
-
- -
+ +
+ +
+
+ +
+ +
+
-
- + +
+ +
+
+ +
+ +
+
+
+ +
+
-
- + + +
+
-
-
-
- + + +
+
-
- + + +
+
-
-
-
- -
-
- -
-
-
- -
-
- -
-
- -
-
- -
+