task-managment / src / server / project / infrastructure / repositories-impl.ts
repositories-impl.ts
Raw
// 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();