// 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 { 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, userId: UserId): Promise { 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 { 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 { 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> { 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; } async getAllProjectMaterials(): Promise { 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 { 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 { const range = getRangeValue(filter?.range); const data = await db .select({ name: project.name, residence: project.residence, month: sql`EXTRACT(MONTH FROM ${project.updatedAt})`, total: sql`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 { 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 { 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 { return (await dbGetOne("floor", { where: eq(floor.id, id.value), })) as Floor; } async updateProjectFloor( data: Partial, userId: UserId ): Promise> { 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> { 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 { 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, userId: UserId ): Promise> { 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 { 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();