feat(client-groups): CM-1 API routes + UI (list, member viewer, copy-emails)
- /api/v1/client-groups (list/create), /[id] (get/patch/delete), /[id]/members (get/set) — route.ts + handlers.ts split, client_groups perms - Client Groups list page (grid + create dialog) and detail page (member viewer, per-row copy email, "Copy all emails" → To:-bar format, manage-members picker over /api/v1/clients) - Sidebar nav entry (UsersRound icon) tsc clean, lint 0 errors, prod build green. Completes CM-1 (Mailchimp push still deferred until client creds/account). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { ClientGroupDetail } from '@/components/client-groups/client-group-detail';
|
||||
|
||||
export default async function ClientGroupDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ portSlug: string; groupId: string }>;
|
||||
}) {
|
||||
const { groupId } = await params;
|
||||
return <ClientGroupDetail groupId={groupId} />;
|
||||
}
|
||||
5
src/app/(dashboard)/[portSlug]/client-groups/page.tsx
Normal file
5
src/app/(dashboard)/[portSlug]/client-groups/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ClientGroupsList } from '@/components/client-groups/client-groups-list';
|
||||
|
||||
export default function ClientGroupsPage() {
|
||||
return <ClientGroupsList />;
|
||||
}
|
||||
49
src/app/api/v1/client-groups/[id]/handlers.ts
Normal file
49
src/app/api/v1/client-groups/[id]/handlers.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { type RouteHandler } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import {
|
||||
archiveClientGroup,
|
||||
getClientGroupById,
|
||||
updateClientGroup,
|
||||
} from '@/lib/services/client-groups.service';
|
||||
import { updateClientGroupSchema } from '@/lib/validators/client-groups';
|
||||
|
||||
export const getHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
const group = await getClientGroupById(params.id!, ctx.portId);
|
||||
return NextResponse.json({ data: group });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const patchHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
const body = await parseBody(req, updateClientGroupSchema);
|
||||
const updated = await updateClientGroup(params.id!, ctx.portId, body, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return NextResponse.json({ data: updated });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
await archiveClientGroup(params.id!, ctx.portId, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
31
src/app/api/v1/client-groups/[id]/members/handlers.ts
Normal file
31
src/app/api/v1/client-groups/[id]/members/handlers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { type RouteHandler } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { listGroupMembers, setGroupMembers } from '@/lib/services/client-groups.service';
|
||||
import { setGroupMembersSchema } from '@/lib/validators/client-groups';
|
||||
|
||||
export const getMembersHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
const members = await listGroupMembers(params.id!, ctx.portId);
|
||||
return NextResponse.json({ data: members, total: members.length });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const putMembersHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
const { clientIds } = await parseBody(req, setGroupMembersSchema);
|
||||
await setGroupMembers(params.id!, ctx.portId, clientIds, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
6
src/app/api/v1/client-groups/[id]/members/route.ts
Normal file
6
src/app/api/v1/client-groups/[id]/members/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
|
||||
import { getMembersHandler, putMembersHandler } from './handlers';
|
||||
|
||||
export const GET = withAuth(withPermission('client_groups', 'view', getMembersHandler));
|
||||
export const PUT = withAuth(withPermission('client_groups', 'manage', putMembersHandler));
|
||||
7
src/app/api/v1/client-groups/[id]/route.ts
Normal file
7
src/app/api/v1/client-groups/[id]/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
|
||||
import { getHandler, patchHandler, deleteHandler } from './handlers';
|
||||
|
||||
export const GET = withAuth(withPermission('client_groups', 'view', getHandler));
|
||||
export const PATCH = withAuth(withPermission('client_groups', 'manage', patchHandler));
|
||||
export const DELETE = withAuth(withPermission('client_groups', 'manage', deleteHandler));
|
||||
31
src/app/api/v1/client-groups/handlers.ts
Normal file
31
src/app/api/v1/client-groups/handlers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { type RouteHandler } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { createClientGroup, listClientGroups } from '@/lib/services/client-groups.service';
|
||||
import { createClientGroupSchema } from '@/lib/validators/client-groups';
|
||||
|
||||
export const listHandler: RouteHandler = async (req, ctx) => {
|
||||
try {
|
||||
const groups = await listClientGroups(ctx.portId);
|
||||
return NextResponse.json({ data: groups, total: groups.length });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createHandler: RouteHandler = async (req, ctx) => {
|
||||
try {
|
||||
const body = await parseBody(req, createClientGroupSchema);
|
||||
const group = await createClientGroup(ctx.portId, body, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return NextResponse.json({ data: group }, { status: 201 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
6
src/app/api/v1/client-groups/route.ts
Normal file
6
src/app/api/v1/client-groups/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
|
||||
import { listHandler, createHandler } from './handlers';
|
||||
|
||||
export const GET = withAuth(withPermission('client_groups', 'view', listHandler));
|
||||
export const POST = withAuth(withPermission('client_groups', 'manage', createHandler));
|
||||
Reference in New Issue
Block a user