// Task Repository Implementation following Hexagonal Architecture
import { getRangeValue } from "@/lib/utils";
import {
and,
gte,
ilike,
eq,
count,
desc,
avg,
sql,
inArray,
lte,
} from "drizzle-orm";
import { jsonAggBuildObject } from "@/helpers/query";
import { dbGetAll, dbGetOne } from "@/lib/db/drizzle-client";
import {
areas,
floor,
materials,
project,
projectMaterials,
task,
} from "@/database/schema";
import { db } from "@/database/db";
import {
Area,
AreaId,
Floor,
FloorId,
Project,
ProjectDetail,
ProjectEarningsResults,
ProjectFilter,
ProjectId,
ProjectQueryItem,
ProjectQueryResuls,
ProjectSummary,
} from "../domain/models";
import { createLogRecord } from "@/server/log";
import { ProjectMaterials } from "@/app/dashboard/stock/types";
import { ProjectRepository } from "../domain/repositories";
import { UserId } from "@/server/user/domain/models";
class ProjectRepositoryImpl implements ProjectRepository {
async create(data: Project, userId: UserId): Promise<Project> {
await db.transaction(async (tx) => {
const createPromise = tx.insert(project).values(data);
const createLogRecordPrimise = createLogRecord(tx, {
userId: userId.value,
eventType: "crear",
tableName: "project",
item: data,
modifiedItem: data?.name,
});
await Promise.all([createPromise, createLogRecordPrimise]);
});
return data as Project;
}
async update(data: Partial<Project>, userId: UserId): Promise<Project> {
await db.transaction(async (tx) => {
const updatePromise = tx
.update(project)
.set(data)
.where(eq(project.id, data.id!));
const createLogRecordPrimise = createLogRecord(tx, {
userId: userId.value,
eventType: "modificar",
tableName: "project",
item: data,
modifiedItem: data as any,
});
await Promise.all([updatePromise, createLogRecordPrimise]);
});
return data as Project;
}
async getAll(filter: ProjectFilter): Promise<ProjectQueryResuls> {
const {
range,
status,
searchQuery,
page = 1,
limit = 10,
includeTasks,
includeAreas,
includeFloor,
} = filter;
const taskRange = range ? getRangeValue(range) : undefined;
const offset = (page - 1) * limit;
const queryFilters = and(
taskRange
? and(
gte(project.updatedAt, taskRange.start),
lte(project.updatedAt, taskRange.end)
)
: undefined,
!status || status === 'all' as any
?undefined
: eq(project.status, status as any),
searchQuery ? ilike(project.name, `%${searchQuery}%`) : undefined
);
let queryFields = {
id: project.id,
name: project.name,
totalCostMaterials: project.totalCostMaterials,
totalCostPerMeter: project.totalCostPerMeter,
laborCost: project.laborCost,
residence: project.residence,
status: project.status,
updatedAt: project.updatedAt,
...(includeTasks && {
tasks: jsonAggBuildObject({
id: task.id,
startDate: task.startDate,
expireDate: task.expireDate,
startedTime: task.startedTime,
endedTime: task.endedTime,
status: task.status,
createdAt: task.createdAT,
comments: task.comments,
projectName: task.projectName,
areaName: task.areaName,
floorName: task.floorName,
projectId: task.projectId,
priority: task.priority,
updatedAt: task.updatedAt,
}),
}),
...(includeFloor && {
floor: jsonAggBuildObject({
id: floor.id,
name: floor.name,
}),
}),
...(includeAreas && {
areas: jsonAggBuildObject({
id: areas.id,
type: areas.type,
floorId: areas.floorId,
}),
}),
};
let query = db
.select(queryFields)
.from(project)
.limit(limit)
.offset(offset)
.where(queryFilters)
.groupBy(project.id);
if (includeTasks) {
query = query.leftJoin(task, eq(project.id, task.projectId));
}
if (includeFloor) {
query = query.leftJoin(floor, eq(project.id, floor.projectId));
}
if (includeAreas) {
query = query.leftJoin(areas, eq(areas.floorId, floor.id));
}
const [results, rawData] = await Promise.all([
db.select({ count: count() }).from(project).where(queryFilters),
query,
]);
const data = rawData.map((project) => {
if (!includeFloor || !includeAreas) return project;
const floors = Array.isArray(project.floor) ? project.floor : [];
const areas = Array.isArray(project.areas) ? project.areas : [];
const uniqueFloors = Array.from(
new Set(floors.map(JSON.stringify as any)),
JSON.parse as any
);
const uniqueAreas = Array.from(
new Set(areas.map(JSON.stringify as any)),
JSON.parse as any
);
const floorsWithAreas = uniqueFloors.map((floor: any) => ({
...floor,
areas: uniqueAreas.filter((area: any) => area?.floorId === floor.id),
}));
return {
...project,
floor: floorsWithAreas,
};
});
const pageCount = Math.ceil(results[0].count / limit);
return {
projects: data as ProjectQueryItem[],
pageCount,
results: results[0].count,
};
}
async getById(projectId: ProjectId): Promise<ProjectDetail> {
const data = await db.query.project.findFirst({
where: eq(project.id, projectId.value),
with: {
floor: {
columns: {
id: true,
name: true,
},
with: {
areas: {
columns: {
id: true,
type: true,
status: true,
width: true,
length: true,
quantity: true,
},
},
},
},
projectMaterials: {
columns: {
id: true,
requiredQuantity: true,
usedQuantity: true,
availableQuantity: true,
},
with: {
materials: {
columns: {
id: true,
name: true,
price: true,
},
},
},
},
task: {
with: {
user: {
columns: {
id: true,
firstName: true,
lastName: true,
},
},
},
},
},
orderBy: [desc(task.id), areas.id, projectMaterials.id],
});
return data as ProjectDetail;
}
async getProductSearch(filter: ProjectFilter): Promise<Partial<Project[]>> {
const data = await db.query.project.findMany({
where: ilike(project.name, `%${filter.searchQuery}%`),
columns: {
id: true,
name: true,
status: true,
},
limit: 6,
orderBy: [desc(project.name)],
});
return data as Partial<Project[]>;
}
async getAllProjectMaterials(): Promise<ProjectMaterials[]> {
const data = await dbGetAll("materials", {
columns: {
id: true,
name: true,
},
with: {
projectMaterials: {
columns: {
id: true,
requiredQuantity: true,
usedQuantity: true,
availableQuantity: true,
},
with: {
materials: {
columns: {
id: true,
name: true,
},
},
project: {
columns: {
id: true,
name: true,
},
},
},
},
},
orderBy: materials.id,
});
return data as ProjectMaterials[];
}
async getProjectSummary(filter: ProjectFilter): Promise<ProjectSummary[]> {
const range = getRangeValue(filter?.range);
const data = await db
.select({
status: project.status,
avg: avg(project.totalCostMaterials),
count: count(project.id),
})
.from(project)
.where(
and(
eq(project.status, "completada"),
range
? and(
gte(project.updatedAt, range.start),
lte(project.updatedAt, range.end)
)
: undefined
)
)
.groupBy(project.status);
const status = [
{ label: "completados", value: "completada" },
{ label: "En proceso", value: "en proceso" },
{ label: "No iniciados", value: "no iniciada" },
];
const statusList = status?.map((s) => {
const match = data?.find((d) => d?.status === s.value);
return {
status: s.value,
count: match ? match.count : 0,
label: s.label,
};
});
return statusList as ProjectSummary[];
}
async getProjectEarnings(
filter: ProjectFilter
): Promise<ProjectEarningsResults> {
const range = getRangeValue(filter?.range);
const data = await db
.select({
name: project.name,
residence: project.residence,
month: sql<number>`EXTRACT(MONTH FROM ${project.updatedAt})`,
total: sql<number>`sum(${project.totalCostPerMeter} + ${project.laborCost} + ${project.totalCostMaterials})`,
})
.from(project)
.where(
and(
eq(project.status, "completada"),
range
? and(
gte(project.updatedAt, range.start),
lte(project.updatedAt, range.end)
)
: undefined
)
)
.groupBy(
sql`EXTRACT(MONTH FROM ${project.updatedAt})`,
project.name,
project.residence
)
.orderBy(sql`EXTRACT(MONTH FROM ${project.updatedAt})`);
const currentMotnIndex = new Date().getMonth() + 1;
const monthsOfYear = [
"enero",
"febrero",
"marzo",
"abril",
"mayo",
"junio",
"julio",
"agosto",
"septiembre",
"octubre",
"noviembre",
"diciembre",
];
const modifiedData = monthsOfYear
.slice(0, currentMotnIndex)
.reduce((acc: any, month: string) => {
const matches = data?.filter(
(item: any) => monthsOfYear[parseInt(item.month) - 1] === month
);
const totalForMonth = matches?.reduce((sum: number, match: any) => {
return sum + (parseFloat(match.total) || 0);
}, 0);
acc.push({ month, total: totalForMonth });
return acc;
}, []);
const totalEarnings = modifiedData.reduce(
(acc: any, current: any) => acc + current?.total,
0
);
return {
projectsEarnings: data as any,
projectsAnalitics: modifiedData ?? [],
totalEarnings: totalEarnings,
};
}
async isFloorInProjectExist(
floorName: string,
projectId: ProjectId
): Promise<boolean> {
const data = await dbGetOne("floor", {
where: and(
eq(floor.name, floorName),
eq(floor.projectId, projectId.value)
),
columns: {
name: true,
},
});
return data ? true : false;
}
async createProjectFloor(data: Floor, userId: UserId): Promise<Floor> {
await db.transaction(async (tx) => {
const createPromise = tx.insert(floor).values(data);
const logRecordPromise = createLogRecord(tx, {
userId: userId.value,
modifiedItem: `floor-${data.name}-${data.projectName}`,
eventType: "crear",
tableName: "floor",
item: data,
});
await Promise.all([createPromise, logRecordPromise]);
});
return data;
}
async getFloorById(id: FloorId): Promise<Floor> {
return (await dbGetOne("floor", {
where: eq(floor.id, id.value),
})) as Floor;
}
async updateProjectFloor(
data: Partial<Floor>,
userId: UserId
): Promise<Partial<Floor>> {
await db.transaction(async (tx) => {
const createPromise = tx.update(floor).set(data);
const logRecordPromise = createLogRecord(tx, {
userId: userId.value,
modifiedItem: `floor-${data?.name}-${data?.projectName}`,
eventType: "crear",
tableName: "floor",
item: data,
});
await Promise.all([createPromise, logRecordPromise]);
return;
});
return data;
}
async deleteProjectFloor(
floorId: FloorId,
userId: UserId
): Promise<Partial<Floor>> {
return await db.transaction(async (tx) => {
const [deletedItem] = await tx
.delete(floor)
.where(eq(floor.id, floorId.value))
.returning();
if (deletedItem) {
await createLogRecord(tx, {
userId: userId.value,
modifiedItem: `floor-${deletedItem.name}-${deletedItem.projectName}`,
eventType: "crear",
tableName: "floor",
item: deletedItem,
});
}
return deletedItem as Floor;
});
}
async createProjectArea(data: Area, userId: UserId): Promise<Area> {
await db.transaction(async (tx) => {
const floor = await this.getFloorById({ value: data.floorId });
const areaPromise = tx
.insert(areas)
.values(data)
.onConflictDoUpdate({
target: areas.id,
set: { quantity: sql`${areas.quantity} + 1` },
});
const logRecordPromise = createLogRecord(tx, {
userId: userId.value,
modifiedItem: `area - ${data.type} / ${floor.name} / ${data.projectName}`,
eventType: "crear",
tableName: "areas",
item: data,
});
await Promise.all([areaPromise, logRecordPromise]);
});
return data;
}
async updateProjectArea(
data: Partial<Area>,
userId: UserId
): Promise<Partial<Area>> {
await db.transaction(async (tx) => {
const floor = await this.getFloorById({ value: data.floorId! });
const areaPromise = await tx
.update(areas)
.set(data)
.where(eq(areas.id, data.id!));
const logRecordPromise = createLogRecord(tx, {
userId: userId.value,
modifiedItem: `area - ${data.type} / ${floor.name} / ${data.projectName}`,
eventType: "modificar",
tableName: "areas",
item: data,
});
await Promise.all([areaPromise, logRecordPromise]);
});
return data;
}
async deleteProjectArea(areaId: AreaId, userId: UserId): Promise<Area> {
const [data] = await db
.delete(areas)
.where(eq(areas.id, areaId.value))
.returning();
await createLogRecord(db, {
userId: userId.value,
modifiedItem: `area - ${data.type} / ${data.projectName}`,
eventType: "eliminar",
tableName: "areas",
item: data,
});
return data;
}
}
export const defaultProjectRepository = new ProjectRepositoryImpl();