Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
de48cdc
working
khanal-samir Apr 10, 2026
efc06cc
Merge remote-tracking branch 'origin/main' into feat/crm-web
khanal-samir Apr 10, 2026
58f0527
chore: remove outdated CRM integration documentation
khanal-samir Apr 10, 2026
7c2c2b8
feat: enhance orgs and people listing with additional data and improv…
khanal-samir Apr 12, 2026
44f3f1d
feat: refactor CRM validation schemas and introduce new types for bet…
khanal-samir Apr 12, 2026
df48c65
feat: simplify organization query hooks and remove unused query key f…
khanal-samir Apr 12, 2026
7c4145c
feat: replace custom debounce hook with usehooks-ts and clean up API …
khanal-samir Apr 13, 2026
b95e4d9
Refactor CRM components to use centralized options and improve code o…
khanal-samir Apr 16, 2026
b9c8c2d
feat: add deals kanban component with drag-and-drop functionality
khanal-samir Apr 16, 2026
45b1083
feat: add description to organizations page header for better context
khanal-samir Apr 16, 2026
8b4064c
feat: remove source field from PersonForm to streamline the form
khanal-samir Apr 16, 2026
8b7068a
feat: add detailed comments to DataTable component for improved code …
khanal-samir Apr 16, 2026
e525bd9
feat: add clearCrmQueries function to remove specific queries on muta…
khanal-samir Apr 16, 2026
72cfba9
feat: refactor CRM view components to use CrmViewField and CrmViewSec…
khanal-samir Apr 16, 2026
1c7d3e4
feat: enhance listDeals function with improved filtering and sorting …
khanal-samir Apr 16, 2026
722cf9e
feat: invalidate ORGS queries on person creation, update, deletion, a…
khanal-samir Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 95 additions & 6 deletions apps/api/src/controllers/deals.controller.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,99 @@
import type { Context } from "hono";
import type { CreateDeal, UpdateDeal } from "@workspace/validators/schemas/crm";
import { and, eq } from "drizzle-orm";
import type { CreateDeal, ListDealsQuery, UpdateDeal } from "@workspace/validators/schemas/crm";
import { and, asc, count, desc, eq, ilike, or } from "drizzle-orm";
import { db } from "@/db/client.js";
import { deals } from "@/db/schema/index.js";
import { deals, orgs, people, user } from "@/db/schema/index.js";
import { STATUS_CODES } from "@/constants/status-codes.js";
import { sendSuccess } from "@/lib/api-response.js";
import { AppError } from "@/lib/app-error.js";
import { toDate } from "@/lib/date.js";
import { getSessionWorkspaceId } from "@/lib/workspace.js";

export async function listDeals(c: Context) {
export async function listDeals(c: Context, query: ListDealsQuery) {
const workspaceId = getSessionWorkspaceId(c);
const results = await db.select().from(deals).where(eq(deals.workspaceId, workspaceId));
const { page, pageSize, sortBy, sortOrder, stage, ownerId, search } = query;
const offset = (page - 1) * pageSize;
const conditions = [eq(deals.workspaceId, workspaceId)];

return sendSuccess(c, { deals: results }, STATUS_CODES.OK);
if (stage) conditions.push(eq(deals.stage, stage));
if (ownerId) conditions.push(eq(deals.ownerId, ownerId));
if (search) {
conditions.push(or(ilike(deals.title, `%${search}%`))!);
}

const whereClause = and(...conditions);

const orderBy = (() => {
switch (sortBy) {
case "value":
return sortOrder === "desc" ? desc(deals.value) : asc(deals.value);
case "currency":
return sortOrder === "desc" ? desc(deals.currency) : asc(deals.currency);
case "stage":
return sortOrder === "desc" ? desc(deals.stage) : asc(deals.stage);
case "closeDate":
return sortOrder === "desc" ? desc(deals.closeDate) : asc(deals.closeDate);
case "createdAt":
return sortOrder === "desc" ? desc(deals.createdAt) : asc(deals.createdAt);
case "updatedAt":
return sortOrder === "desc" ? desc(deals.updatedAt) : asc(deals.updatedAt);
case "title":
default:
return sortOrder === "desc" ? desc(deals.title) : asc(deals.title);
}
})();

const [results, totalCountRows] = await Promise.all([
db
.select({
id: deals.id,
workspaceId: deals.workspaceId,
orgId: deals.orgId,
personId: deals.personId,
ownerId: deals.ownerId,
title: deals.title,
value: deals.value,
currency: deals.currency,
stage: deals.stage,
closeDate: deals.closeDate,
createdAt: deals.createdAt,
updatedAt: deals.updatedAt,
orgName: orgs.name,
personName: people.name,
ownerName: user.name,
})
.from(deals)
.leftJoin(orgs, eq(deals.orgId, orgs.id))
.leftJoin(people, eq(deals.personId, people.id))
.leftJoin(user, eq(deals.ownerId, user.id))
.where(whereClause)
.orderBy(orderBy)
.limit(pageSize)
.offset(offset),
db.select({ totalCount: count() }).from(deals).where(whereClause),
]);

const totalCount = Number(totalCountRows[0]?.totalCount ?? 0);
const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / pageSize);

return sendSuccess(
c,
{
deals: results.map((deal) => ({
...deal,
orgName: deal.orgName ?? null,
personName: deal.personName ?? null,
ownerName: deal.ownerName ?? null,
})),
meta: {
page,
pageSize,
totalCount,
totalPages,
},
},
STATUS_CODES.OK,
);
}

export async function getDeal(c: Context, id: string) {
Expand All @@ -33,8 +113,15 @@ export async function getDeal(c: Context, id: string) {
name: true,
},
},
owner: {
columns: {
id: true,
name: true,
},
},
},
});

if (!deal) {
throw new AppError("Deal not found", STATUS_CODES.NOT_FOUND);
}
Expand Down Expand Up @@ -81,8 +168,10 @@ export async function deleteDeal(c: Context, id: string) {
.delete(deals)
.where(and(eq(deals.id, id), eq(deals.workspaceId, workspaceId)))
.returning();

if (!deal) {
throw new AppError("Deal not found", STATUS_CODES.NOT_FOUND);
}

return sendSuccess(c, { deal }, STATUS_CODES.OK);
}
93 changes: 87 additions & 6 deletions apps/api/src/controllers/orgs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,89 @@
import type { Context } from "hono";
import type { CreateOrg, UpdateOrg } from "@workspace/validators/schemas/crm";
import { and, eq } from "drizzle-orm";
import type {
BulkDeleteInput,
CreateOrg,
ListOrgsQuery,
UpdateOrg,
} from "@workspace/validators/schemas/crm";
import { and, asc, count, desc, eq, ilike, inArray, or } from "drizzle-orm";
import { db } from "@/db/client.js";
import { orgs } from "@/db/schema/index.js";
import { orgs, people } from "@/db/schema/index.js";
import { STATUS_CODES } from "@/constants/status-codes.js";
import { sendSuccess } from "@/lib/api-response.js";
import { AppError } from "@/lib/app-error.js";
import { getSessionWorkspaceId } from "@/lib/workspace.js";

export async function listOrgs(c: Context) {
export async function listOrgs(c: Context, query: ListOrgsQuery) {
const workspaceId = getSessionWorkspaceId(c);
const results = await db.select().from(orgs).where(eq(orgs.workspaceId, workspaceId));
const { page, pageSize, sortOrder, sortBy, search, industry, size } = query;

return sendSuccess(c, { orgs: results }, STATUS_CODES.OK);
const conditions = [eq(orgs.workspaceId, workspaceId)];
if (industry) conditions.push(eq(orgs.industry, industry));
if (size) conditions.push(eq(orgs.size, size));
if (search) {
const searchTerm = `%${search}%`;
conditions.push(
or(
ilike(orgs.name, searchTerm),
ilike(orgs.domain, searchTerm),
ilike(orgs.industry, searchTerm),
ilike(orgs.size, searchTerm),
ilike(orgs.location, searchTerm),
)!,
);
}

const whereClause = and(...conditions);

const direction = sortOrder === "desc" ? desc : asc;
const orderBy = (() => {
switch (sortBy) {
case "domain":
return direction(orgs.domain);
case "industry":
return direction(orgs.industry);
case "size":
return direction(orgs.size);
case "location":
return direction(orgs.location);
case "createdAt":
return direction(orgs.createdAt);
case "updatedAt":
return direction(orgs.updatedAt);
case "name":
default:
return direction(orgs.name);
}
})();

const [rows, totalCountResult, peopleCounts] = await Promise.all([
db
.select()
.from(orgs)
.where(whereClause)
.orderBy(orderBy)
.limit(pageSize)
.offset((page - 1) * pageSize),
db.select({ totalCount: count() }).from(orgs).where(whereClause),
db
.select({ orgId: people.orgId, count: count() })
.from(people)
.where(eq(people.workspaceId, workspaceId))
.groupBy(people.orgId),
]);

const totalCount = Number(totalCountResult[0]?.totalCount ?? 0);
const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / pageSize);
const peopleCountMap = new Map(peopleCounts.map((r) => [r.orgId, r.count]));

return sendSuccess(
c,
{
orgs: rows.map((org) => ({ ...org, peopleCount: peopleCountMap.get(org.id) ?? 0 })),
meta: { page, pageSize, totalCount, totalPages },
},
STATUS_CODES.OK,
);
}

export async function getOrg(c: Context, id: string) {
Expand Down Expand Up @@ -80,3 +151,13 @@ export async function deleteOrg(c: Context, id: string) {

return sendSuccess(c, { org }, STATUS_CODES.OK);
}

export async function bulkDeleteOrgs(c: Context, payload: BulkDeleteInput) {
const workspaceId = getSessionWorkspaceId(c);
const deletedOrgs = await db
.delete(orgs)
.where(and(eq(orgs.workspaceId, workspaceId), inArray(orgs.id, payload.ids)))
.returning({ id: orgs.id });

return sendSuccess(c, { deleted: deletedOrgs.length }, STATUS_CODES.OK);
}
127 changes: 116 additions & 11 deletions apps/api/src/controllers/people.controller.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,122 @@
import type { Context } from "hono";
import type { CreatePerson, UpdatePerson } from "@workspace/validators/schemas/crm";
import { and, eq } from "drizzle-orm";
import type {
BulkDeleteInput,
CreatePerson,
ListPeopleQuery,
UpdatePerson,
} from "@workspace/validators/schemas/crm";
import { and, asc, count, desc, eq, ilike, or, inArray } from "drizzle-orm";
import { db } from "@/db/client.js";
import { people } from "@/db/schema/index.js";
import { orgs, people, user } from "@/db/schema/index.js";
import { STATUS_CODES } from "@/constants/status-codes.js";
import { sendSuccess } from "@/lib/api-response.js";
import { AppError } from "@/lib/app-error.js";
import { toDate } from "@/lib/date.js";
import { getSessionWorkspaceId } from "@/lib/workspace.js";

export async function listPeople(c: Context) {
export async function listPeople(c: Context, query: ListPeopleQuery) {
const workspaceId = getSessionWorkspaceId(c);
const results = await db.select().from(people).where(eq(people.workspaceId, workspaceId));
const { page, pageSize, sortBy, sortOrder, status, source, ownerId, search } = query;
const offset = (page - 1) * pageSize;

return sendSuccess(c, { people: results }, STATUS_CODES.OK);
}
const conditions = [eq(people.workspaceId, workspaceId)];
if (status) conditions.push(eq(people.status, status));
if (source) conditions.push(eq(people.source, source));
if (ownerId) conditions.push(eq(people.ownerId, ownerId));
if (search) {
conditions.push(
or(
ilike(people.name, `%${search}%`),
ilike(people.email, `%${search}%`),
ilike(people.phone, `%${search}%`),
ilike(people.jobTitle, `%${search}%`),
)!,
);
}

const whereClause = and(...conditions);

const sortColumnMap = {
name: people.name,
email: people.email,
phone: people.phone,
jobTitle: people.jobTitle,
status: people.status,
source: people.source,
lastContactedAt: people.lastContactedAt,
createdAt: people.createdAt,
updatedAt: people.updatedAt,
} as const;

const orderColumn = sortColumnMap[sortBy] ?? people.createdAt;
const orderBy = sortOrder === "desc" ? desc(orderColumn) : asc(orderColumn);

const [rows, totalCountResult] = await Promise.all([
db
.select({
id: people.id,
workspaceId: people.workspaceId,
orgId: people.orgId,
ownerId: people.ownerId,
name: people.name,
email: people.email,
phone: people.phone,
jobTitle: people.jobTitle,
linkedinUrl: people.linkedinUrl,
status: people.status,
source: people.source,
lastContactedAt: people.lastContactedAt,
customFields: people.customFields,
createdAt: people.createdAt,
updatedAt: people.updatedAt,
orgName: orgs.name,
ownerName: user.name,
})
.from(people)
.leftJoin(orgs, eq(people.orgId, orgs.id))
.leftJoin(user, eq(people.ownerId, user.id))
.where(whereClause)
.orderBy(orderBy)
.limit(pageSize)
.offset(offset),
db.select({ totalCount: count() }).from(people).where(whereClause),
]);

const totalCount = Number(totalCountResult[0]?.totalCount ?? 0);
const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / pageSize);

return sendSuccess(
c,
{
people: rows.map((row) => ({
...row,
orgName: row.orgName ?? null,
ownerName: row.ownerName ?? null,
})),
meta: { page, pageSize, totalCount, totalPages },
},
STATUS_CODES.OK,
);
}
export async function getPerson(c: Context, id: string) {
const workspaceId = getSessionWorkspaceId(c);
const [person] = await db
.select()
.from(people)
.where(and(eq(people.id, id), eq(people.workspaceId, workspaceId)));
const person = await db.query.people.findFirst({
where: and(eq(people.id, id), eq(people.workspaceId, workspaceId)),
with: {
org: {
columns: {
id: true,
name: true,
},
},
owner: {
columns: {
id: true,
name: true,
},
},
},
});

if (!person) {
throw new AppError("Person not found", STATUS_CODES.NOT_FOUND);
Expand Down Expand Up @@ -76,3 +171,13 @@ export async function deletePerson(c: Context, id: string) {

return sendSuccess(c, { person }, STATUS_CODES.OK);
}

export async function bulkDeletePeople(c: Context, payload: BulkDeleteInput) {
const workspaceId = getSessionWorkspaceId(c);
const deletedPeople = await db
.delete(people)
.where(and(eq(people.workspaceId, workspaceId), inArray(people.id, payload.ids)))
.returning({ id: people.id });

return sendSuccess(c, { deleted: deletedPeople.length }, STATUS_CODES.OK);
}
Loading
Loading