From 260131d8fd8fc51c9564ce78b92381218740b0a9 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 13:07:36 +0530 Subject: [PATCH 01/13] fix: Restrict member management actions to Owner/Admin roles --- .../(app)/[orgId]/people/all/components/MemberRow.tsx | 8 ++++---- .../people/all/components/PendingInvitationRow.tsx | 5 +++-- .../[orgId]/people/all/components/TeamMembers.tsx | 10 ++++++++++ .../people/all/components/TeamMembersClient.tsx | 6 +++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 93a0ca0b7..3f7fa6384 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -43,6 +43,7 @@ interface MemberRowProps { member: MemberWithUser; onRemove: (memberId: string) => void; onUpdateRole: (memberId: string, roles: Role[]) => void; + canEdit: boolean; } // Helper to get initials @@ -60,7 +61,7 @@ function getInitials(name?: string | null, email?: string | null): string { return '??'; } -export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { +export function MemberRow({ member, onRemove, onUpdateRole, canEdit}: MemberRowProps) { const params = useParams<{ orgId: string }>(); const { orgId } = params; @@ -89,7 +90,6 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { ) as Role[]; const isOwner = currentRoles.includes('owner'); - const canEditRoles = true; const canRemove = !isOwner; const isEmployee = currentRoles.includes('employee'); @@ -183,7 +183,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { variant="ghost" size="sm" className="h-8 w-8 p-0" - disabled={!canEditRoles} + disabled={!canEdit} > @@ -199,7 +199,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { } }} > - {canEditRoles && ( + {canEdit && ( Promise; + canCancel: boolean; } -export function PendingInvitationRow({ invitation, onCancel }: PendingInvitationRowProps) { +export function PendingInvitationRow({ invitation, onCancel, canCancel }: PendingInvitationRowProps) { const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -105,7 +106,7 @@ export function PendingInvitationRow({ invitation, onCancel }: PendingInvitation ))} - + @@ -241,6 +243,7 @@ export function TeamMembersClient({ member={member as MemberWithUser} onRemove={handleRemoveMember} onUpdateRole={handleUpdateRole} + canEdit={canManageMembers} /> ))} @@ -255,6 +258,7 @@ export function TeamMembersClient({ key={invitation.displayId} invitation={invitation as Invitation} onCancel={handleCancelInvitation} + canCancel={canManageMembers} /> ))} From 3848a656a2b9da5c990b37f3bc214e8524f687e8 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 13:51:57 +0530 Subject: [PATCH 02/13] fix: Update member role check --- .../[employeeId]/actions/update-employee.ts | 46 +++++++++++++++++-- .../components/EmployeeDetails.tsx | 6 ++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts index cee6a511a..a7f786d4b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts @@ -29,7 +29,35 @@ export const updateEmployee = authActionClient const { employeeId, name, email, department, isActive, createdAt } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error(appErrors.UNAUTHORIZED.message); + if (!organizationId) { + return { + success: false, + error: { + code: appErrors.UNAUTHORIZED, + message: appErrors.UNAUTHORIZED.message, + }, + }; + } + + const currentUserMember = await db.member.findFirst({ + where: { + organizationId: organizationId, + userId: ctx.user.id, + }, + }); + + if ( + !currentUserMember || + (!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner')) + ) { + return { + success: false, + error: { + code: appErrors.UNAUTHORIZED, + message: "You don't have permission to update members.", + }, + }; + } const member = await db.member.findUnique({ where: { @@ -40,7 +68,13 @@ export const updateEmployee = authActionClient }); if (!member || !member.user) { - throw new Error(appErrors.NOT_FOUND.message); + return { + success: false, + error: { + code: appErrors.NOT_FOUND, + message: appErrors.NOT_FOUND.message, + }, + }; } const memberUpdateData: { @@ -110,7 +144,13 @@ export const updateEmployee = authActionClient if (error.code === 'P2002') { const targetFields = error.meta?.target as string[] | undefined; if (targetFields?.includes('email')) { - throw new Error('Email address is already in use.'); + return { + success: false, + error: { + code: appErrors.UNEXPECTED_ERROR, + message: 'Email address is already in use.', + }, + }; } } } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx index df5cc75f0..82949af69 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx @@ -48,7 +48,11 @@ export const EmployeeDetails = ({ }); const { execute, status: actionStatus } = useAction(updateEmployee, { - onSuccess: () => { + onSuccess: (res) => { + if (!res?.data?.success) { + toast.error(res?.data?.error?.message || 'Failed to update employee details'); + return; + } toast.success('Employee details updated successfully'); }, onError: (error) => { From 1024b670e99768a7992fc1c10e3792d953d1047c Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 13:52:07 +0530 Subject: [PATCH 03/13] chore: Remove unused code --- .../actions/get-employee-details.ts | 69 -------------- .../[employeeId]/actions/update-department.ts | 80 ---------------- .../actions/update-employee-details.ts | 91 ------------------- .../actions/update-employee-status.ts | 85 ----------------- .../components/EditableDepartment.tsx | 63 ------------- .../components/EditableDetails.tsx | 23 ----- .../components/EditableStatus.tsx | 57 ------------ 7 files changed, 468 deletions(-) delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDetails.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableStatus.tsx diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts deleted file mode 100644 index aa95073b8..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts +++ /dev/null @@ -1,69 +0,0 @@ -'use server'; - -import { authActionClient } from '@/actions/safe-action'; -import { auth } from '@/utils/auth'; -import { db } from '@db'; -import { headers } from 'next/headers'; -import { type AppError, appErrors, employeeDetailsInputSchema } from '../types'; - -// Type-safe action response -export type ActionResponse = Promise< - { success: true; data: T } | { success: false; error: AppError } ->; - -export const getEmployeeDetails = authActionClient - .inputSchema(employeeDetailsInputSchema) - .metadata({ - name: 'get-employee-details', - track: { - event: 'get-employee-details', - channel: 'server', - }, - }) - .action(async ({ parsedInput }) => { - const { employeeId } = parsedInput; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - throw new Error('Organization ID not found'); - } - - try { - const employee = await db.member.findUnique({ - where: { - id: employeeId, - organizationId, - }, - select: { - id: true, - department: true, - createdAt: true, - isActive: true, - user: true, - }, - }); - - if (!employee) { - return { - success: false, - error: appErrors.NOT_FOUND.message, - }; - } - - return { - success: true, - data: employee, - }; - } catch (error) { - console.error('Error fetching employee details:', error); - return { - success: false, - error: appErrors.UNEXPECTED_ERROR.message, - }; - } - }); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts deleted file mode 100644 index 13f166395..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts +++ /dev/null @@ -1,80 +0,0 @@ -'use server'; - -import { authActionClient } from '@/actions/safe-action'; -import { auth } from '@/utils/auth'; -import type { Departments } from '@db'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { type AppError, appErrors, updateEmployeeDepartmentSchema } from '../types'; - -export type ActionResponse = Promise< - { success: true; data: T } | { success: false; error: AppError } ->; - -export const updateEmployeeDepartment = authActionClient - .inputSchema(updateEmployeeDepartmentSchema) - .metadata({ - name: 'update-employee-department', - track: { - event: 'update-employee-department', - channel: 'server', - }, - }) - .action(async ({ parsedInput }): Promise => { - const { employeeId, department } = parsedInput; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - return { - success: false, - error: appErrors.UNAUTHORIZED, - }; - } - - try { - const employee = await db.member.findUnique({ - where: { - id: employeeId, - organizationId, - }, - }); - - if (!employee) { - return { - success: false, - error: appErrors.NOT_FOUND, - }; - } - - const updatedEmployee = await db.member.update({ - where: { - id: employeeId, - organizationId, - }, - data: { - department: department as Departments, - }, - }); - - // Revalidate related paths - revalidatePath(`/${organizationId}/people/${employeeId}`); - revalidatePath(`/${organizationId}/people`); - - return { - success: true, - data: updatedEmployee, - }; - } catch (error) { - console.error('Error updating employee department:', error); - return { - success: false, - error: appErrors.UNEXPECTED_ERROR, - }; - } - }); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts deleted file mode 100644 index 39e12a432..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts +++ /dev/null @@ -1,91 +0,0 @@ -'use server'; - -import { authActionClient } from '@/actions/safe-action'; -import { auth } from '@/utils/auth'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { appErrors } from '../types'; - -const schema = z.object({ - employeeId: z.string(), - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email address'), -}); - -export const updateEmployeeDetails = authActionClient - .inputSchema(schema) - .metadata({ - name: 'update-employee-details', - track: { - event: 'update-employee-details', - channel: 'server', - }, - }) - .action( - async ({ - parsedInput, - }): Promise<{ success: true; data: any } | { success: false; error: any }> => { - const { employeeId, name, email } = parsedInput; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - return { - success: false, - error: appErrors.UNAUTHORIZED, - }; - } - - try { - const employee = await db.member.findUnique({ - where: { - id: employeeId, - organizationId, - }, - }); - - if (!employee) { - return { - success: false, - error: appErrors.NOT_FOUND, - }; - } - - const updatedEmployee = await db.member.update({ - where: { - id: employeeId, - organizationId, - }, - data: { - user: { - update: { - name, - email, - }, - }, - }, - }); - - // Revalidate related paths - revalidatePath(`/${organizationId}/people/${employeeId}`); - revalidatePath(`/${organizationId}/people`); - - return { - success: true, - data: updatedEmployee, - }; - } catch (error) { - console.error('Error updating employee details:', error); - return { - success: false, - error: appErrors.UNEXPECTED_ERROR, - }; - } - }, - ); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts deleted file mode 100644 index bde8ccf74..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts +++ /dev/null @@ -1,85 +0,0 @@ -'use server'; - -import { authActionClient } from '@/actions/safe-action'; -import { auth } from '@/utils/auth'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { appErrors } from '../types'; - -const schema = z.object({ - employeeId: z.string(), - isActive: z.boolean(), -}); - -export const updateEmployeeStatus = authActionClient - .inputSchema(schema) - .metadata({ - name: 'update-employee-status', - track: { - event: 'update-employee-status', - channel: 'server', - }, - }) - .action( - async ({ - parsedInput, - }): Promise<{ success: true; data: any } | { success: false; error: any }> => { - const { employeeId, isActive } = parsedInput; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - return { - success: false, - error: appErrors.UNAUTHORIZED, - }; - } - - try { - const employee = await db.member.findUnique({ - where: { - id: employeeId, - organizationId, - }, - }); - - if (!employee) { - return { - success: false, - error: appErrors.NOT_FOUND, - }; - } - - const updatedEmployee = await db.member.update({ - where: { - id: employeeId, - organizationId, - }, - data: { - isActive, - }, - }); - - // Revalidate related paths - revalidatePath(`/${organizationId}/people/${employeeId}`); - revalidatePath(`/${organizationId}/people`); - - return { - success: true, - data: updatedEmployee, - }; - } catch (error) { - console.error('Error updating employee status:', error); - return { - success: false, - error: appErrors.UNEXPECTED_ERROR, - }; - } - }, - ); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx deleted file mode 100644 index 2f503b158..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'use client'; - -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; -import type { Departments } from '@db'; -import { useAction } from 'next-safe-action/hooks'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { updateEmployeeDepartment } from '../actions/update-department'; - -const DEPARTMENTS = [ - { value: 'admin', label: 'Admin' }, - { value: 'gov', label: 'Governance' }, - { value: 'hr', label: 'HR' }, - { value: 'it', label: 'IT' }, - { value: 'itsm', label: 'IT Service Management' }, - { value: 'qms', label: 'Quality Management' }, - { value: 'none', label: 'None' }, -]; - -interface EditableDepartmentProps { - employeeId: string; - currentDepartment: Departments; - onSuccess?: () => void; -} - -export function EditableDepartment({ - employeeId, - currentDepartment, - onSuccess, -}: EditableDepartmentProps) { - const [department, setDepartment] = useState(currentDepartment); - - const { execute, status } = useAction(updateEmployeeDepartment, { - onSuccess: () => { - toast.success('Department updated successfully'); - onSuccess?.(); - }, - onError: (error) => { - toast.error(error?.error?.serverError || 'Failed to update department'); - }, - }); - - const handleSave = () => { - execute({ employeeId, department }); - }; - - return ( -
- -
- ); -} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDetails.tsx deleted file mode 100644 index 34b62158a..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDetails.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -interface EditableDetailsProps { - employeeId: string; - currentName: string; - currentEmail: string; - onSuccess?: () => void; -} - -export function EditableDetails({ employeeId, currentName, currentEmail }: EditableDetailsProps) { - return ( -
-
- Name - {currentName} -
-
- Email - {currentEmail} -
-
- ); -} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableStatus.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableStatus.tsx deleted file mode 100644 index 7163c2463..000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableStatus.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import type { EmployeeStatusType } from '@/components/tables/people/employee-status'; -import { getEmployeeStatusFromBoolean } from '@/components/tables/people/employee-status'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; -import { useAction } from 'next-safe-action/hooks'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { updateEmployeeStatus } from '../actions/update-employee-status'; - -const STATUS_OPTIONS = [ - { value: 'active', label: 'Active' }, - { value: 'inactive', label: 'Inactive' }, -]; - -interface EditableStatusProps { - employeeId: string; - currentStatus: boolean; - onSuccess?: () => void; -} - -export function EditableStatus({ employeeId, currentStatus, onSuccess }: EditableStatusProps) { - const initialStatus = getEmployeeStatusFromBoolean(currentStatus); - const [status, setStatus] = useState(initialStatus); - - const { execute, status: actionStatus } = useAction(updateEmployeeStatus, { - onSuccess: () => { - toast.success('Employee status updated successfully'); - onSuccess?.(); - }, - onError: (error) => { - toast.error(error?.error?.serverError || 'Failed to update employee status'); - }, - }); - - const handleSave = () => { - const isActive = status === 'active'; - execute({ employeeId, isActive }); - }; - - return ( -
- -
- ); -} From 194b462fe1b46233206a52062187fa6ce5f5614a Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 14:09:36 +0530 Subject: [PATCH 04/13] fix: Validate user role for adding employee --- .../all/actions/addEmployeeWithoutInvite.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 027701778..4ccd429b7 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 @@ -3,6 +3,7 @@ import { auth } from '@/utils/auth'; import type { Role } from '@db'; import { db } from '@db'; +import { headers } from 'next/headers'; export const addEmployeeWithoutInvite = async ({ email, @@ -14,6 +15,25 @@ export const addEmployeeWithoutInvite = async ({ roles: Role[]; }) => { try { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.session) { + throw new Error('Authentication required.'); + } + const currentUserId = session.session.userId; + const currentUserMember = await db.member.findFirst({ + where: { + organizationId: organizationId, + userId: currentUserId, + }, + }); + + if ( + !currentUserMember || + (!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner')) + ) { + throw new Error("You don't have permission to add members."); + } + let userId = ''; const existingUser = await db.user.findUnique({ where: { From 2a9b769ee62d0c7ba9b0e22db927115cf7f0d63c Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 14:12:21 +0530 Subject: [PATCH 05/13] chore: Formatting --- .../people/[employeeId]/actions/update-employee.ts | 14 +++++++------- .../[orgId]/people/all/components/MemberRow.tsx | 2 +- .../people/all/components/PendingInvitationRow.tsx | 12 ++++++++++-- .../[orgId]/people/all/components/TeamMembers.tsx | 10 +++++----- .../src/app/(app)/[orgId]/tasks/[taskId]/page.tsx | 5 +---- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts index a7f786d4b..d902f73ac 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts @@ -50,13 +50,13 @@ export const updateEmployee = authActionClient !currentUserMember || (!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner')) ) { - return { - success: false, - error: { - code: appErrors.UNAUTHORIZED, - message: "You don't have permission to update members.", - }, - }; + return { + success: false, + error: { + code: appErrors.UNAUTHORIZED, + message: "You don't have permission to update members.", + }, + }; } const member = await db.member.findUnique({ diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 3f7fa6384..5528a1c27 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -61,7 +61,7 @@ function getInitials(name?: string | null, email?: string | null): string { return '??'; } -export function MemberRow({ member, onRemove, onUpdateRole, canEdit}: MemberRowProps) { +export function MemberRow({ member, onRemove, onUpdateRole, canEdit }: MemberRowProps) { const params = useParams<{ orgId: string }>(); const { orgId } = params; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx index 0b8a69c7c..ff561a63d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx @@ -31,7 +31,11 @@ interface PendingInvitationRowProps { canCancel: boolean; } -export function PendingInvitationRow({ invitation, onCancel, canCancel }: PendingInvitationRowProps) { +export function PendingInvitationRow({ + invitation, + onCancel, + canCancel, +}: PendingInvitationRowProps) { const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -106,7 +110,11 @@ export function PendingInvitationRow({ invitation, onCancel, canCancel }: Pendin ))} - + + + + e.stopPropagation()}> + + Delete Vendor + + Are you sure you want to delete {vendor.name}? This action cannot be + undone. + + + + Cancel + + {isDeleting ? 'Deleting...' : 'Delete'} + + + + + + ); +}; From bcf4434ff0513e40554e7d548d68ccfded55826f Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 23 Aug 2025 19:35:48 +0530 Subject: [PATCH 12/13] feat: Add sorting options for organization list --- .../src/components/organization-switcher.tsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/organization-switcher.tsx b/apps/app/src/components/organization-switcher.tsx index f77315f82..b5bea0042 100644 --- a/apps/app/src/components/organization-switcher.tsx +++ b/apps/app/src/components/organization-switcher.tsx @@ -13,12 +13,13 @@ import { CommandSeparator, } from '@comp/ui/command'; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@comp/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import type { Organization } from '@db'; import { Check, ChevronsUpDown, Loader2, Plus, Search } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useRouter } from 'next/navigation'; import { useQueryState } from 'nuqs'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; interface OrganizationSwitcherProps { organizations: Organization[]; @@ -88,6 +89,27 @@ export function OrganizationSwitcher({ const router = useRouter(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [pendingOrgId, setPendingOrgId] = useState(null); + const [sortOrder, setSortOrder] = useState('alphabetical'); + + useEffect(() => { + const savedSortOrder = localStorage.getItem('org-sort-order'); + if (savedSortOrder) { + setSortOrder(savedSortOrder); + } + }, []); + + useEffect(() => { + localStorage.setItem('org-sort-order', sortOrder); + }, [sortOrder]); + + const sortedOrganizations = [...organizations].sort((a, b) => { + if (sortOrder === 'alphabetical') { + return (a.name).localeCompare(b.name); + } else if (sortOrder === 'recent') { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + } + return 0; + }); const [showOrganizationSwitcher, setShowOrganizationSwitcher] = useQueryState( 'showOrganizationSwitcher', @@ -177,10 +199,21 @@ export function OrganizationSwitcher({ className="placeholder:text-muted-foreground flex h-11 w-full rounded-md border-0 bg-transparent py-3 text-sm outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" /> +
+ +
No results found - {organizations.map((org) => ( + {sortedOrganizations.map((org) => ( Date: Sat, 23 Aug 2025 20:54:44 +0530 Subject: [PATCH 13/13] feat: Update tasks on top level --- .../app/(app)/[orgId]/tasks/[taskId]/page.tsx | 5 +- .../[orgId]/tasks/actions/updateTaskStatus.ts | 68 +++++++++++++++++ .../[orgId]/tasks/components/TaskCard.tsx | 67 +++++++---------- .../tasks/components/TaskStatusSelector.tsx | 75 +++++++++++++++++++ apps/app/src/app/(app)/[orgId]/tasks/page.tsx | 5 -- 5 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/actions/updateTaskStatus.ts create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/components/TaskStatusSelector.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx index b2387fec8..7392ab283 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx @@ -20,10 +20,7 @@ export default async function TaskPage({ }); console.log('[TaskPage] Session obtained, fetching data'); - const [task, members] = await Promise.all([ - getTask(taskId, session), - getMembers(orgId, session), - ]); + const [task, members] = await Promise.all([getTask(taskId, session), getMembers(orgId, session)]); if (!task) { redirect(`/${orgId}/tasks`); diff --git a/apps/app/src/app/(app)/[orgId]/tasks/actions/updateTaskStatus.ts b/apps/app/src/app/(app)/[orgId]/tasks/actions/updateTaskStatus.ts new file mode 100644 index 000000000..6f88ee2ba --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/actions/updateTaskStatus.ts @@ -0,0 +1,68 @@ +'use server'; + +import { authActionClient } from '@/actions/safe-action'; +import { db } from '@db'; +import { revalidatePath } from 'next/cache'; +import { z } from 'zod'; + +const updateTaskStatusSchema = z.object({ + id: z.string(), + status: z.enum(['todo', 'in_progress', 'done', 'not_relevant']), +}); + +export const updateTaskStatusAction = authActionClient + .inputSchema(updateTaskStatusSchema) + .metadata({ + name: 'update-task-status', + track: { + event: 'update_task_status', + description: 'Update Task Status from List View', + channel: 'server', + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { id, status } = parsedInput; + const { activeOrganizationId } = ctx.session; + + if (!activeOrganizationId) { + return { + success: false, + error: 'Not authorized', + }; + } + + try { + const task = await db.task.findUnique({ + where: { + id, + organizationId: activeOrganizationId, + }, + }); + + if (!task) { + return { + success: false, + error: 'Task not found', + }; + } + + // Update the task status + await db.task.update({ + where: { id }, + data: { status }, + }); + + // Revalidate paths to update UI + revalidatePath(`/${activeOrganizationId}/tasks`); + + return { + success: true, + }; + } catch (error) { + console.error(error); + return { + success: false, + error: 'Failed to update task status', + }; + } + }); diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskCard.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskCard.tsx index c03c04cb6..684de15d0 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskCard.tsx @@ -1,11 +1,11 @@ 'use client'; -import type { Member, Task, TaskStatus, User } from '@db'; +import type { Member, Task, User } from '@db'; import Image from 'next/image'; import { usePathname, useRouter } from 'next/navigation'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import { TaskStatusIndicator } from './TaskStatusIndicator'; +import { TaskStatusSelector } from './TaskStatusSelector'; // DnD Item Type identifier for tasks. export const ItemTypes = { @@ -85,8 +85,6 @@ export function TaskCard({ return; } - const dragIndex = item.index; - const hoverIndex = index; const clientOffset = monitor.getClientOffset(); if (!clientOffset) return; @@ -151,20 +149,6 @@ export function TaskCard({ return members.find((m) => m.id === task.assigneeId); }, [task.assigneeId, members]); - // Helper to get Tailwind class for the entity type indicator dot. - const getEntityTypeDotClass = (entityType: 'control' | 'risk' | 'vendor'): string => { - switch (entityType) { - case 'control': - return 'bg-blue-500'; - case 'risk': - return 'bg-red-500'; - case 'vendor': - return 'bg-green-500'; - default: - return 'bg-gray-500'; - } - }; - // Navigation handler for clicking on the task card. const handleNavigate = () => { const targetPath = `${pathname}/${task.id}`; @@ -174,9 +158,8 @@ export function TaskCard({ return (
{/* Reorder Indicator (Solid Line) */} @@ -203,26 +186,32 @@ export function TaskCard({
::
-
- +
+
- {task.title} -
- Apr 15 -
- {assignedMember?.user?.image ? ( - {assignedMember.user.name - ) : ( - - {assignedMember?.user?.name?.charAt(0) ?? '?'} - - )} + +
+ {task.title} +
+ Apr 15 +
+ {assignedMember?.user?.image ? ( + {assignedMember.user.name + ) : ( + + {assignedMember?.user?.name?.charAt(0) ?? '?'} + + )} +
diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskStatusSelector.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskStatusSelector.tsx new file mode 100644 index 000000000..77853e00b --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskStatusSelector.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { Button } from '@comp/ui/button'; +import type { Task, TaskStatus } from '@db'; +import { useAction } from 'next-safe-action/hooks'; +import { toast } from 'sonner'; +import { PropertySelector } from '../[taskId]/components/PropertySelector'; +import { taskStatuses } from '../[taskId]/components/constants'; +import { updateTaskStatusAction } from '../actions/updateTaskStatus'; +import { TaskStatusIndicator } from './TaskStatusIndicator'; + +interface TaskStatusSelectorProps { + task: Task; +} + +export function TaskStatusSelector({ task }: TaskStatusSelectorProps) { + const { execute, status } = useAction(updateTaskStatusAction, { + onSuccess: (res) => { + if (res?.data.success) { + return; + } else { + console.error('Failed to update task status:', res?.data.error); + toast.error(`Failed to update task status: ${res?.data.error || 'Unknown error'}`); + } + }, + onError: (error) => { + console.error('Failed to update task status:', error); + toast.error('Failed to update task status.'); + }, + }); + + const isUpdating = status === 'executing'; + + const handleStatusSelect = (selectedStatus: string | null) => { + if (selectedStatus && selectedStatus !== task.status) { + execute({ + id: task.id, + status: selectedStatus as TaskStatus, + }); + } + }; + + return ( + + value={task.status} + options={taskStatuses} + getKey={(status) => status} + renderOption={(status) => ( +
+ + {status.replace('_', ' ')} +
+ )} + onSelect={handleStatusSelect} + trigger={ + + } + searchPlaceholder="Change status..." + emptyText="No status found." + contentWidth="w-48" + disabled={isUpdating} + /> + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/tasks/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/page.tsx index 67441bc66..162fd59ce 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/page.tsx @@ -38,9 +38,6 @@ export default async function TasksPage({ } const getTasks = async (statusParam?: string) => { - console.log('Fetching tasks...', { - statusParam, - }); const session = await auth.api.getSession({ headers: await headers(), }); @@ -59,7 +56,6 @@ const getTasks = async (statusParam?: string) => { // Filter by Status (using passed argument) if (typeof statusParam === 'string' && statusParam in TaskStatus) { whereClause.status = statusParam as TaskStatus; - console.log(`Filtering by status: ${whereClause.status}`); } const tasks = await db.task.findMany({ @@ -70,7 +66,6 @@ const getTasks = async (statusParam?: string) => { }; const getMembersWithMetadata = async () => { - console.log('Fetching members...'); const session = await auth.api.getSession({ headers: await headers(), });