feat: add teams/groups with dashboard scoping
Schema: - teams table (name unique, description) - team_members table (team_id, user_id, unique constraint) - team_id column on dashboards API: - GET/POST/PATCH/DELETE /teams - POST /teams/:id/members (add user) - DELETE /teams/:id/members/:userId (remove user) - dashboards support team_id on create/update Frontend: - Teams tab in admin: CRUD + member management with add/remove - Sidebar: dashboards filtered to user's teams (unassigned dashboards visible to all) - Compact dashboard picker dropdown in sidebar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,8 @@ import {
|
||||
PanelLeftIcon,
|
||||
CommandIcon,
|
||||
} from "lucide-react";
|
||||
import { getTickets, getQueues, getViews, getDashboards, getUsers, createDashboard } from "@/lib/api";
|
||||
import type { Dashboard, Queue, SavedView, User } from "@/lib/types";
|
||||
import { getTickets, getQueues, getViews, getDashboards, getUsers, getTeams, createDashboard } from "@/lib/api";
|
||||
import type { Dashboard, Queue, SavedView, Team, User } from "@/lib/types";
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -100,48 +100,58 @@ function SidebarNav() {
|
||||
const [addingDashboard, setAddingDashboard] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Find current user and compute view counts
|
||||
Promise.all([getTickets(), getUsers()]).then(([ticketRes, userRes]) => {
|
||||
async function load() {
|
||||
// Find current user
|
||||
const [ticketRes, userRes] = await Promise.all([getTickets(), getUsers()]);
|
||||
const data = ticketRes.data;
|
||||
const users = userRes.data ?? [];
|
||||
const currentUser = users.find((u) => u.username !== 'system') ?? users[0] ?? null;
|
||||
if (currentUser) setCurrentUserId(currentUser.id);
|
||||
const myId = currentUser?.id ?? null;
|
||||
setCurrentUserId(myId);
|
||||
|
||||
if (data) {
|
||||
const myId = currentUser?.id;
|
||||
const now = Date.now();
|
||||
const week = 7 * 24 * 60 * 60 * 1000;
|
||||
setCounts({
|
||||
all: data.length,
|
||||
my: myId ? data.filter((t) => t.owner_id === myId).length : 0,
|
||||
unassigned: data.filter((t) => !t.owner_id).length,
|
||||
recent: data.filter(
|
||||
(t) => new Date(t.updated_at).getTime() > now - week
|
||||
).length,
|
||||
recent: data.filter((t) => new Date(t.updated_at).getTime() > now - week).length,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
getQueues().then(({ data }) => {
|
||||
if (data) {
|
||||
Promise.all(
|
||||
data.map((q) =>
|
||||
// Queues
|
||||
const queueRes = await getQueues();
|
||||
if (queueRes.data) {
|
||||
const qs = await Promise.all(
|
||||
queueRes.data.map((q) =>
|
||||
getTickets({ queue_id: q.id }).then(({ data: tickets }) => ({
|
||||
...q,
|
||||
count: tickets?.length ?? 0,
|
||||
}))
|
||||
)
|
||||
).then(setQueues);
|
||||
);
|
||||
setQueues(qs);
|
||||
}
|
||||
});
|
||||
|
||||
getViews().then(({ data }) => {
|
||||
if (data) setSavedViews(data);
|
||||
});
|
||||
// Views
|
||||
const viewRes = await getViews();
|
||||
if (viewRes.data) setSavedViews(viewRes.data);
|
||||
|
||||
getDashboards().then(({ data }) => {
|
||||
if (data) setDashboards(data);
|
||||
});
|
||||
// Dashboards scoped to user's teams
|
||||
const [dashRes, teamRes] = await Promise.all([getDashboards(), getTeams()]);
|
||||
const allDashboards = dashRes.data ?? [];
|
||||
const allTeams = teamRes.data ?? [];
|
||||
const userTeams = allTeams.filter((t) =>
|
||||
(t.members ?? []).some((m) => m.id === myId)
|
||||
);
|
||||
const teamIds = new Set(userTeams.map((t) => t.id));
|
||||
const visible = allDashboards.filter((d) =>
|
||||
!d.team_id || teamIds.has(d.team_id)
|
||||
);
|
||||
setDashboards(visible);
|
||||
}
|
||||
void load();
|
||||
}, []);
|
||||
|
||||
const collapsed = useSidebarCollapsed();
|
||||
|
||||
Reference in New Issue
Block a user