diff --git a/apps/api/src/auth/hybrid-auth.guard.ts b/apps/api/src/auth/hybrid-auth.guard.ts index 0e027e873..91441bc71 100644 --- a/apps/api/src/auth/hybrid-auth.guard.ts +++ b/apps/api/src/auth/hybrid-auth.guard.ts @@ -232,6 +232,7 @@ export class HybridAuthGuard implements CanActivate { where: { userId, organizationId, + deactivated: false, }, select: { id: true, diff --git a/apps/api/src/comments/comments.service.ts b/apps/api/src/comments/comments.service.ts index 7d245dd8b..864ce6774 100644 --- a/apps/api/src/comments/comments.service.ts +++ b/apps/api/src/comments/comments.service.ts @@ -116,6 +116,7 @@ export class CommentsService { name: comment.author.user.name, email: comment.author.user.email, image: comment.author.user.image, + deactivated: comment.author.deactivated, }, attachments, createdAt: comment.createdAt, @@ -154,6 +155,7 @@ export class CommentsService { where: { userId, organizationId, + deactivated: false, }, include: { user: true, @@ -211,6 +213,7 @@ export class CommentsService { name: member.user.name, email: member.user.email, image: member.user.image, + deactivated: member.deactivated, }, attachments: result.attachments, createdAt: result.comment.createdAt, @@ -284,6 +287,7 @@ export class CommentsService { name: existingComment.author.user.name, email: existingComment.author.user.email, image: existingComment.author.user.image, + deactivated: existingComment.author.deactivated, }, attachments, createdAt: updatedComment.createdAt, diff --git a/apps/api/src/comments/dto/comment-responses.dto.ts b/apps/api/src/comments/dto/comment-responses.dto.ts index e98f366ce..dcf7ddcd0 100644 --- a/apps/api/src/comments/dto/comment-responses.dto.ts +++ b/apps/api/src/comments/dto/comment-responses.dto.ts @@ -89,6 +89,13 @@ export class AuthorResponseDto { nullable: true, }) image: string | null; + + @ApiProperty({ + description: 'Whether the user is deactivated', + example: false, + nullable: true, + }) + deactivated: boolean; } export class CommentResponseDto { diff --git a/apps/api/src/devices/devices.service.ts b/apps/api/src/devices/devices.service.ts index 0bbaa4307..0d977c9f0 100644 --- a/apps/api/src/devices/devices.service.ts +++ b/apps/api/src/devices/devices.service.ts @@ -97,6 +97,7 @@ export class DevicesService { where: { id: memberId, organizationId: organizationId, + deactivated: false, }, select: { id: true, @@ -165,6 +166,7 @@ export class DevicesService { where: { id: memberId, organizationId: organizationId, + deactivated: false, }, select: { id: true, diff --git a/apps/api/src/people/utils/member-queries.ts b/apps/api/src/people/utils/member-queries.ts index 0985195c5..37c019e65 100644 --- a/apps/api/src/people/utils/member-queries.ts +++ b/apps/api/src/people/utils/member-queries.ts @@ -40,7 +40,7 @@ export class MemberQueries { organizationId: string, ): Promise { return db.member.findMany({ - where: { organizationId }, + where: { organizationId, deactivated: false }, select: this.MEMBER_SELECT, orderBy: { createdAt: 'desc' }, }); diff --git a/apps/api/src/people/utils/member-validator.ts b/apps/api/src/people/utils/member-validator.ts index 1c678c63f..8236990ae 100644 --- a/apps/api/src/people/utils/member-validator.ts +++ b/apps/api/src/people/utils/member-validator.ts @@ -47,6 +47,7 @@ export class MemberValidator { where: { id: memberId, organizationId, + deactivated: false, }, select: { id: true, userId: true }, }); @@ -71,6 +72,7 @@ export class MemberValidator { const whereClause: any = { userId, organizationId, + deactivated: false, }; if (excludeMemberId) { diff --git a/apps/app/src/actions/add-comment.ts b/apps/app/src/actions/add-comment.ts index 40a92dbf7..0ef626f17 100644 --- a/apps/app/src/actions/add-comment.ts +++ b/apps/app/src/actions/add-comment.ts @@ -38,6 +38,7 @@ export const addCommentAction = authActionClient where: { userId: session.userId, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/change-organization.ts b/apps/app/src/actions/change-organization.ts index 32e768bb0..9dc48ea53 100644 --- a/apps/app/src/actions/change-organization.ts +++ b/apps/app/src/actions/change-organization.ts @@ -28,6 +28,7 @@ export const changeOrganizationAction = authActionClient where: { userId: user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index 78e0bf7ac..a507fbf6d 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -69,6 +69,7 @@ export const completeInvitation = authActionClientWithoutOrg where: { userId: user.id, organizationId: invitation.organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/organization/get-organization-users-action.ts b/apps/app/src/actions/organization/get-organization-users-action.ts index dcd33e6a8..aaa9d2e49 100644 --- a/apps/app/src/actions/organization/get-organization-users-action.ts +++ b/apps/app/src/actions/organization/get-organization-users-action.ts @@ -26,6 +26,7 @@ export const getOrganizationUsersAction = authActionClient const users = await db.member.findMany({ where: { organizationId: ctx.session.activeOrganizationId, + deactivated: false, }, select: { user: { diff --git a/apps/app/src/actions/policies/accept-requested-policy-changes.ts b/apps/app/src/actions/policies/accept-requested-policy-changes.ts index f60e30acc..a3d716756 100644 --- a/apps/app/src/actions/policies/accept-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/accept-requested-policy-changes.ts @@ -82,6 +82,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient where: { organizationId: session.activeOrganizationId, isActive: true, + deactivated: false, }, include: { user: true, @@ -131,6 +132,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient where: { userId: user.id, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/create-new-policy.ts b/apps/app/src/actions/policies/create-new-policy.ts index 9f7e75a82..146528fb6 100644 --- a/apps/app/src/actions/policies/create-new-policy.ts +++ b/apps/app/src/actions/policies/create-new-policy.ts @@ -39,6 +39,7 @@ export const createPolicyAction = authActionClient where: { userId: user.id, organizationId: activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/deny-requested-policy-changes.ts b/apps/app/src/actions/policies/deny-requested-policy-changes.ts index 5c937edbe..a61dcec38 100644 --- a/apps/app/src/actions/policies/deny-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/deny-requested-policy-changes.ts @@ -68,6 +68,7 @@ export const denyRequestedPolicyChangesAction = authActionClient where: { userId: user.id, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/publish-all.ts b/apps/app/src/actions/policies/publish-all.ts index 8ce0cdf79..8a17e6c51 100644 --- a/apps/app/src/actions/policies/publish-all.ts +++ b/apps/app/src/actions/policies/publish-all.ts @@ -41,6 +41,7 @@ export const publishAllPoliciesAction = authActionClient where: { userId: user.id, organizationId: parsedInput.organizationId, + deactivated: false, }, }); @@ -104,6 +105,7 @@ export const publishAllPoliciesAction = authActionClient where: { organizationId: parsedInput.organizationId, isActive: true, + deactivated: false, OR: [ { role: { contains: Role.employee } }, { role: { contains: Role.contractor } }, diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 656010ebb..49280a4ea 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -231,6 +231,7 @@ export const authWithOrgAccessClient = authActionClient.use(async ({ next, clien where: { userId: ctx.user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx index d26062890..7c87b44eb 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx @@ -39,6 +39,7 @@ export default async function DashboardPage({ params }: { params: Promise<{ orgI where: { userId: session.user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index c54c4ac87..50cc6268c 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -56,6 +56,7 @@ export default async function Layout({ where: { userId: session.user.id, organizationId: requestedOrgId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts index 5ee3eae37..66d1da59d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts @@ -25,6 +25,7 @@ export const addEmployeeWithoutInvite = async ({ where: { organizationId: organizationId, userId: currentUserId, + deactivated: false, }, }); @@ -57,16 +58,45 @@ export const addEmployeeWithoutInvite = async ({ userId = newUser.id; } - const member = await auth.api.addMember({ - body: { - userId: existingUser?.id ?? userId, + const finalUserId = existingUser?.id ?? userId; + + // Check if there's an existing member (including deactivated ones) for this user and organization + const existingMember = await db.member.findFirst({ + where: { + userId: finalUserId, organizationId, - role: roles, // Auth API expects role or role array }, }); - // Create training video completion entries for the new member - if (member?.id) { + let member; + if (existingMember) { + // If member exists but is deactivated, reactivate it and update roles + if (existingMember.deactivated) { + const roleString = roles.sort().join(','); + member = await db.member.update({ + where: { id: existingMember.id }, + data: { + deactivated: false, + role: roleString, + }, + }); + } else { + // Member already exists and is active, return existing member + member = existingMember; + } + } else { + // No existing member, create a new one + member = await auth.api.addMember({ + body: { + userId: finalUserId, + organizationId, + role: roles, // Auth API expects role or role array + }, + }); + } + + // Create training video completion entries for the new member (only if member was just created/reactivated) + if (member?.id && !existingMember) { await createTrainingVideoEntries(member.id); } diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts index a734a9124..822eef671 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; // Adjust safe-action import for colocalized structure import { authActionClient } from '@/actions/safe-action'; import type { ActionResponse } from '@/actions/types'; +import { sendUnassignedItemsNotificationEmail, type UnassignedItem } from '@comp/email'; const removeMemberSchema = z.object({ memberId: z.string(), @@ -36,6 +37,7 @@ export const removeMember = authActionClient where: { organizationId: ctx.session.activeOrganizationId, userId: ctx.user.id, + deactivated: false, }, }); @@ -55,6 +57,9 @@ export const removeMember = authActionClient id: memberId, organizationId: ctx.session.activeOrganizationId, }, + include: { + user: true, + }, }); if (!targetMember) { @@ -80,11 +85,148 @@ export const removeMember = authActionClient }; } - // Remove the member - await db.member.delete({ + // Get organization name + const organization = await db.organization.findUnique({ + where: { + id: ctx.session.activeOrganizationId, + }, + select: { + name: true, + }, + }); + + // Check for assignments and collect unassigned items + const unassignedItems: UnassignedItem[] = []; + + // Check tasks + const assignedTasks = await db.task.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + title: true, + }, + }); + + for (const task of assignedTasks) { + unassignedItems.push({ + type: 'task', + id: task.id, + name: task.title, + }); + } + + // Check policies + const assignedPolicies = await db.policy.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + name: true, + }, + }); + + for (const policy of assignedPolicies) { + unassignedItems.push({ + type: 'policy', + id: policy.id, + name: policy.name, + }); + } + + // Check risks + const assignedRisks = await db.risk.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + title: true, + }, + }); + + for (const risk of assignedRisks) { + unassignedItems.push({ + type: 'risk', + id: risk.id, + name: risk.title, + }); + } + + // Check vendors + const assignedVendors = await db.vendor.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + name: true, + }, + }); + + for (const vendor of assignedVendors) { + unassignedItems.push({ + type: 'vendor', + id: vendor.id, + name: vendor.name, + }); + } + + // Clear all assignments + await Promise.all([ + db.task.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.policy.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.risk.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.vendor.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + ]); + + // Mark the member as deactivated instead of deleting + await db.member.update({ where: { id: memberId, }, + data: { + deactivated: true, + isActive: false, + }, }); // Consider if deleting sessions is still desired here @@ -94,6 +236,34 @@ export const removeMember = authActionClient }, }); + // Notify admins if there are unassigned items + if (unassignedItems.length > 0 && organization) { + const owner = await db.member.findFirst({ + where: { + organizationId: ctx.session.activeOrganizationId, + role: { contains: 'owner' }, + deactivated: false, + }, + include: { + user: true, + }, + }); + + const removedMemberName = targetMember.user.name || targetMember.user.email || 'Member'; + + if (owner) { + // Send email to the org owner + sendUnassignedItemsNotificationEmail({ + email: owner.user.email, + userName: owner.user.name || owner.user.email || 'Owner', + organizationName: organization.name, + organizationId: ctx.session.activeOrganizationId, + removedMemberName, + unassignedItems, + }); + } + } + revalidatePath(`/${ctx.session.activeOrganizationId}/settings/users`); revalidateTag(`user_${ctx.user.id}`); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts index 8558f1805..d826135a4 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts @@ -37,6 +37,7 @@ export const revokeInvitation = authActionClient where: { organizationId: ctx.session.activeOrganizationId, userId: ctx.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx index fdeff7173..ed20bda8b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx @@ -45,6 +45,7 @@ export async function TeamMembers() { const fetchedMembers = await db.member.findMany({ where: { organizationId: organizationId, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 358470b3a..545542d8f 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -140,7 +140,7 @@ export function TeamMembersClient({ const handleRemoveMember = async (memberId: string) => { const result = await removeMemberAction({ memberId }); - if (result?.data) { + if (result?.data?.success) { // Success case toast.success('has been removed from the organization'); router.refresh(); // Add client-side refresh as well diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx index 09a5cd1c5..ca9537d3c 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx @@ -41,6 +41,7 @@ export async function EmployeesOverview() { const fetchedMembers = await db.member.findMany({ where: { organizationId: organizationId, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts index 3ec1d54f5..965348591 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts @@ -23,6 +23,7 @@ export const getEmployeeDevices: () => Promise = async () => { const employees = await db.member.findMany({ where: { organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/layout.tsx b/apps/app/src/app/(app)/[orgId]/people/layout.tsx index 149b80caf..44640914e 100644 --- a/apps/app/src/app/(app)/[orgId]/people/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/layout.tsx @@ -18,6 +18,7 @@ export default async function Layout({ children }: { children: React.ReactNode } const allMembers = await db.member.findMany({ where: { organizationId: orgId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx index f9af8cb7b..0a78bb0cd 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx @@ -3,10 +3,17 @@ import { Badge } from '@comp/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; import { ScrollArea } from '@comp/ui/scroll-area'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@comp/ui/tooltip'; import { AuditLog, AuditLogEntityType } from '@db'; import { format } from 'date-fns'; import { ActivityIcon, + AlertTriangle, CalendarIcon, ClockIcon, FileIcon, @@ -90,6 +97,7 @@ const getUserInfo = (log: AuditLogWithRelations) => { name: log.user.name, email: log.user.email, avatarUrl: log.user.image || undefined, + deactivated: log.member?.deactivated || false, }; } @@ -98,6 +106,7 @@ const getUserInfo = (log: AuditLogWithRelations) => { name: undefined, email: undefined, avatarUrl: undefined, + deactivated: false, }; }; @@ -110,10 +119,26 @@ const LogItem = ({ log }: { log: AuditLogWithRelations }) => {
- - - {getInitials(userInfo.name)} - +
+ + + {getInitials(userInfo.name)} + + {userInfo.deactivated && ( + + + +
+ +
+
+ +

This user is deactivated.

+
+
+
+ )} +
diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts index 314c0cb43..2c9bb3a50 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts @@ -137,6 +137,7 @@ export const getAssignees = async () => { role: { notIn: ['employee', 'contractor'], }, + deactivated: false, }, include: { user: true, @@ -193,6 +194,7 @@ export const getComments = async (policyId: string): Promise ({ id: att.id, diff --git a/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx b/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx index 7273c18c7..175ce7e03 100644 --- a/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx @@ -116,6 +116,7 @@ const getAssignees = cache(async () => { where: { organizationId: session.session.activeOrganizationId, isActive: true, + deactivated: false, role: { notIn: ['employee', 'contractor'], }, diff --git a/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx b/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx index 7ad8e795e..5fe1a4560 100644 --- a/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx @@ -91,6 +91,7 @@ const getAssignees = cache(async () => { role: { notIn: ['employee', 'contractor'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx index ab8fa7923..79d23ea54 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx @@ -7,7 +7,7 @@ import { Check, Circle, FolderTree, List, Plus, XCircle } from 'lucide-react'; import Image from 'next/image'; import { useParams } from 'next/navigation'; import { useQueryState } from 'nuqs'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { CreateTaskSheet } from './CreateTaskSheet'; import { ModernTaskList } from './ModernTaskList'; import { SearchInput } from './SearchInput'; @@ -51,7 +51,31 @@ export function TaskList({ const [statusFilter, setStatusFilter] = useQueryState('status'); const [assigneeFilter, setAssigneeFilter] = useQueryState('assignee'); const [createTaskOpen, setCreateTaskOpen] = useQueryState('create-task'); + + // Initialize with default, load from localStorage after hydration const [activeTab, setActiveTab] = useState<'categories' | 'list'>('categories'); + const lastLoadedOrgId = useRef(null); + + // Load saved preference from localStorage after client-side hydration + useEffect(() => { + // Reset and load preference when orgId changes or on initial load + if (lastLoadedOrgId.current !== orgId) { + const saved = localStorage.getItem(`task-view-preference-${orgId}`); + if (saved === 'categories' || saved === 'list') { + setActiveTab(saved); + } else { + // Reset to default if no saved preference exists for this org + setActiveTab('categories'); + } + lastLoadedOrgId.current = orgId; + } + }, [orgId]); + + // Save preference to localStorage when user changes it (not on initial load) + const handleTabChange = (tab: 'categories' | 'list') => { + setActiveTab(tab); + localStorage.setItem(`task-view-preference-${orgId}`, tab); + }; const eligibleAssignees = useMemo(() => { return members @@ -88,7 +112,9 @@ export function TaskList({ // Calculate overall stats from all tasks (not filtered) const overallStats = useMemo(() => { const total = initialTasks.length; - const done = initialTasks.filter((t) => t.status === 'done' || t.status === 'not_relevant').length; + const done = initialTasks.filter( + (t) => t.status === 'done' || t.status === 'not_relevant', + ).length; const inProgress = initialTasks.filter((t) => t.status === 'in_progress').length; const todo = initialTasks.filter((t) => t.status === 'todo').length; const completionRate = total > 0 ? Math.round((done / total) * 100) : 0; @@ -621,7 +647,7 @@ export function TaskList({ {/* View Toggle */}