diff --git a/erd.png b/erd.png new file mode 100644 index 0000000..b0879fc Binary files /dev/null and b/erd.png differ diff --git a/generate_erd.py b/generate_erd.py new file mode 100644 index 0000000..6c697b8 --- /dev/null +++ b/generate_erd.py @@ -0,0 +1,214 @@ +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from matplotlib.patches import FancyBboxPatch +import matplotlib.patheffects as pe + +# ── colour palette ────────────────────────────────────────────────────────── +C = { + "admin": "#4A90D9", + "user": "#2ECC71", + "village": "#E74C3C", + "announce": "#F39C12", + "project": "#9B59B6", + "division": "#1ABC9C", + "discuss": "#E67E22", + "other": "#95A5A6", + "new": "#E74C3C", +} + +GROUPS = { + "Admin": (["AdminRole","Admin"], C["admin"]), + "User": (["UserRole","User","TokenDeviceUser","UserLog"],C["user"]), + "Village": (["Village","ColorTheme","BannerImage"], C["village"]), + "Announcement": (["Announcement","AnnouncementMember", + "AnnouncementFile"], C["announce"]), + "Project": (["Project","ProjectMember","ProjectFile", + "ProjectLink","ProjectTask","ProjectTaskFile", + "ProjectTaskDetail"], C["project"]), + "Division": (["Division","DivisionMember","DivisionProject", + "DivisionProjectMember","DivisionProjectFile", + "DivisionProjectLink", + "DivisionProjectTask","DivisionProjectTaskFile", + "DivisionProjectTaskDetail", + "DivisionDisscussion","DivisionDisscussionComment", + "DivisionDiscussionFile", + "DivisionDocumentFolderFile","DivisionDocumentShare", + "DivisionCalendar","DivisionCalendarReminder", + "DivisionCalendarMember", + "ContainerFileDivision"], C["division"]), + "Discussion": (["Discussion","DiscussionMember", + "DiscussionComment","DiscussionFile"], C["discuss"]), + "Other": (["Group","Position","Notifications", + "Subscribe","Setting","ContainerImage"], C["other"]), +} + +NEW_MODELS = {"ProjectTaskFile", "DivisionProjectTaskFile"} + +# ── relations (src, dst, label) ───────────────────────────────────────────── +RELATIONS = [ + # Admin + ("AdminRole","Admin","1-N"), + # User + ("UserRole","User","1-N"), + ("Village","User","1-N"), + ("Group","User","1-N"), + ("Position","User","0..1-N"), + # Village + ("Village","Group","1-N"), + ("Village","Announcement","1-N"), + ("Village","Project","1-N"), + ("Village","Division","1-N"), + ("Village","Discussion","1-N"), + ("Village","ColorTheme","1-N"), + ("Village","BannerImage","1-N"), + # Group + ("Group","Position","1-N"), + ("Group","Project","1-N"), + ("Group","Division","1-N"), + ("Group","AnnouncementMember","1-N"), + ("Group","Discussion","1-N"), + # Announcement + ("Announcement","AnnouncementMember","1-N"), + ("Announcement","AnnouncementFile","1-N"), + ("Division","AnnouncementMember","1-N"), + # Project + ("Project","ProjectMember","1-N"), + ("Project","ProjectFile","1-N"), + ("Project","ProjectLink","1-N"), + ("Project","ProjectTask","1-N"), + ("ProjectTask","ProjectTaskDetail","1-N"), + ("ProjectTask","ProjectTaskFile","1-N"), + ("ProjectFile","ProjectTaskFile","1-N"), + # Division + ("Division","DivisionMember","1-N"), + ("Division","DivisionProject","1-N"), + ("DivisionProject","DivisionProjectMember","1-N"), + ("DivisionProject","DivisionProjectFile","1-N"), + ("DivisionProject","DivisionProjectLink","1-N"), + ("DivisionProject","DivisionProjectTask","1-N"), + ("DivisionProjectTask","DivisionProjectTaskDetail","1-N"), + ("DivisionProjectTask","DivisionProjectTaskFile","1-N"), + ("DivisionProjectFile","DivisionProjectTaskFile","1-N"), + ("ContainerFileDivision","DivisionProjectFile","1-N"), + ("Division","DivisionDisscussion","1-N"), + ("DivisionDisscussion","DivisionDisscussionComment","1-N"), + ("DivisionDisscussion","DivisionDiscussionFile","1-N"), + ("Division","DivisionDocumentFolderFile","1-N"), + ("DivisionDocumentFolderFile","DivisionDocumentShare","1-N"), + ("Division","DivisionCalendar","1-N"), + ("DivisionCalendar","DivisionCalendarReminder","1-N"), + ("DivisionCalendar","DivisionCalendarMember","1-N"), + # Discussion + ("Discussion","DiscussionMember","1-N"), + ("Discussion","DiscussionComment","1-N"), + ("Discussion","DiscussionFile","1-N"), + # Other + ("User","Notifications","1-N"), + ("User","Subscribe","1-1"), + ("User","TokenDeviceUser","1-N"), + ("User","UserLog","1-N"), +] + +# ── layout: group boxes ────────────────────────────────────────────────────── +# (x, y, w, h) in data coordinates (canvas = 0..100 x 0..100) +LAYOUT = { + "Admin": ( 1, 88, 18, 10), + "User": ( 1, 68, 22, 18), + "Village": (26, 88, 22, 10), + "Other": (51, 88, 22, 10), + "Announcement": (76, 80, 22, 18), + "Project": ( 1, 2, 38, 48), + "Division": (41, 2, 38, 64), + "Discussion": (81, 2, 17, 30), +} + +def group_center(gname): + x,y,w,h = LAYOUT[gname] + return x+w/2, y+h/2 + +def model_pos(model): + for gname,(models,_) in GROUPS.items(): + if model in models: + x,y,w,h = LAYOUT[gname] + idx = models.index(model) + n = len(models) + cols = max(1, min(3, n)) + rows = (n + cols - 1) // cols + col = idx % cols + row = idx // cols + mx = x + 1.5 + col * (w-2) / cols + my = y + h - 2.5 - row * (h-1.5) / rows + return mx, my + return 50, 50 + +# ── draw ───────────────────────────────────────────────────────────────────── +fig, ax = plt.subplots(figsize=(28, 22)) +ax.set_xlim(0, 100) +ax.set_ylim(0, 100) +ax.axis("off") +fig.patch.set_facecolor("#F0F4F8") +ax.set_facecolor("#F0F4F8") + +ax.set_title("ERD – Sistem Desa Mandiri", fontsize=20, fontweight="bold", + color="#2C3E50", pad=14) + +# group boxes +for gname, (models, color) in GROUPS.items(): + x,y,w,h = LAYOUT[gname] + rect = FancyBboxPatch((x,y), w, h, + boxstyle="round,pad=0.3", + linewidth=2, edgecolor=color, + facecolor=color+"22") + ax.add_patch(rect) + ax.text(x+w/2, y+h-0.6, gname, ha="center", va="top", + fontsize=9, fontweight="bold", color=color) + +# model nodes +for gname, (models, color) in GROUPS.items(): + for m in models: + mx, my = model_pos(m) + is_new = m in NEW_MODELS + fc = "#FFECEC" if is_new else "white" + ec = C["new"] if is_new else color + lw = 2.5 if is_new else 1.5 + node = FancyBboxPatch((mx-3.2, my-0.85), 6.4, 1.7, + boxstyle="round,pad=0.2", + linewidth=lw, edgecolor=ec, facecolor=fc) + ax.add_patch(node) + fw = "bold" if is_new else "normal" + ax.text(mx, my, m, ha="center", va="center", + fontsize=6.2, color="#2C3E50", fontweight=fw) + +# relations +drawn = set() +for src, dst, lbl in RELATIONS: + key = tuple(sorted([src,dst])) + sx, sy = model_pos(src) + dx, dy = model_pos(dst) + color = "#BDC3C7" + is_new_rel = src in NEW_MODELS or dst in NEW_MODELS + if is_new_rel: + color = C["new"] + ax.annotate("", xy=(dx,dy), xytext=(sx,sy), + arrowprops=dict(arrowstyle="-|>", color=color, + lw=1.5 if is_new_rel else 0.8, + connectionstyle="arc3,rad=0.05")) + if key not in drawn: + mx2, my2 = (sx+dx)/2, (sy+dy)/2 + ax.text(mx2, my2+0.4, lbl, ha="center", va="bottom", + fontsize=4.5, color=color, alpha=0.85) + drawn.add(key) + +# legend +leg_items = [ + mpatches.Patch(facecolor="#FFECEC", edgecolor=C["new"], linewidth=2, + label="Model Baru"), + mpatches.Patch(facecolor="white", edgecolor="#BDC3C7", label="Model Lama"), +] +ax.legend(handles=leg_items, loc="lower right", fontsize=9, + framealpha=0.9, edgecolor="#BDC3C7") + +out = "/Users/wibu04/Documents/Projects/sistem-desa-mandiri/erd.png" +plt.savefig(out, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor()) +plt.close() +print("Saved:", out) diff --git a/prisma/migrations/20260505093957_add_task_file/migration.sql b/prisma/migrations/20260505093957_add_task_file/migration.sql new file mode 100644 index 0000000..1f7a4db --- /dev/null +++ b/prisma/migrations/20260505093957_add_task_file/migration.sql @@ -0,0 +1,35 @@ +-- CreateTable +CREATE TABLE "ProjectTaskFile" ( + "id" TEXT NOT NULL, + "idTask" TEXT NOT NULL, + "idFile" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProjectTaskFile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DivisionProjectTaskFile" ( + "id" TEXT NOT NULL, + "idTask" TEXT NOT NULL, + "idFile" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DivisionProjectTaskFile_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ProjectTaskFile" ADD CONSTRAINT "ProjectTaskFile_idTask_fkey" FOREIGN KEY ("idTask") REFERENCES "ProjectTask"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectTaskFile" ADD CONSTRAINT "ProjectTaskFile_idFile_fkey" FOREIGN KEY ("idFile") REFERENCES "ProjectFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DivisionProjectTaskFile" ADD CONSTRAINT "DivisionProjectTaskFile_idTask_fkey" FOREIGN KEY ("idTask") REFERENCES "DivisionProjectTask"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DivisionProjectTaskFile" ADD CONSTRAINT "DivisionProjectTaskFile_idFile_fkey" FOREIGN KEY ("idFile") REFERENCES "DivisionProjectFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c8c9f7f..8a8c332 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -232,15 +232,16 @@ model ProjectMember { } model ProjectFile { - id String @id @default(cuid()) - Project Project @relation(fields: [idProject], references: [id]) - idProject String - name String - extension String - idStorage String? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + Project Project @relation(fields: [idProject], references: [id]) + idProject String + name String + extension String + idStorage String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ProjectTaskFile ProjectTaskFile[] } model ProjectLink { @@ -267,6 +268,18 @@ model ProjectTask { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt ProjectTaskDetail ProjectTaskDetail[] + ProjectTaskFile ProjectTaskFile[] +} + +model ProjectTaskFile { + id String @id @default(cuid()) + ProjectTask ProjectTask @relation(fields: [idTask], references: [id]) + idTask String + ProjectFile ProjectFile @relation(fields: [idFile], references: [id]) + idFile String + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model ProjectTaskDetail { @@ -368,6 +381,7 @@ model DivisionProjectTask { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt DivisionProjectTaskDetail DivisionProjectTaskDetail[] + DivisionProjectTaskFile DivisionProjectTaskFile[] } model DivisionProjectTaskDetail { @@ -397,18 +411,30 @@ model DivisionProjectMember { } model DivisionProjectFile { - id String @id @default(cuid()) - Division Division @relation(fields: [idDivision], references: [id]) - idDivision String - DivisionProject DivisionProject @relation(fields: [idProject], references: [id]) - idProject String - ContainerFileDivision ContainerFileDivision @relation(fields: [idFile], references: [id]) - idFile String - isActive Boolean @default(true) - User User @relation(fields: [createdBy], references: [id]) - createdBy String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + Division Division @relation(fields: [idDivision], references: [id]) + idDivision String + DivisionProject DivisionProject @relation(fields: [idProject], references: [id]) + idProject String + ContainerFileDivision ContainerFileDivision @relation(fields: [idFile], references: [id]) + idFile String + isActive Boolean @default(true) + User User @relation(fields: [createdBy], references: [id]) + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + DivisionProjectTaskFile DivisionProjectTaskFile[] +} + +model DivisionProjectTaskFile { + id String @id @default(cuid()) + DivisionProjectTask DivisionProjectTask @relation(fields: [idTask], references: [id]) + idTask String + DivisionProjectFile DivisionProjectFile @relation(fields: [idFile], references: [id]) + idFile String + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model DivisionDisscussion { diff --git a/src/app/api/mobile/project/[id]/route.ts b/src/app/api/mobile/project/[id]/route.ts index 9f0152a..52f272f 100644 --- a/src/app/api/mobile/project/[id]/route.ts +++ b/src/app/api/mobile/project/[id]/route.ts @@ -68,7 +68,18 @@ export async function GET(request: Request, context: { params: { id: string } }) status: true, dateStart: true, dateEnd: true, - createdAt: true + createdAt: true, + ProjectTaskFile: { + where: { isActive: true }, + select: { + ProjectFile: { + select: { + name: true, + extension: true + } + } + } + } }, orderBy: { dateStart: 'asc' @@ -76,12 +87,15 @@ export async function GET(request: Request, context: { params: { id: string } }) }) const formatData = dataProgress.map((v: any) => ({ - ..._.omit(v, ["dateStart", "dateEnd", "createdAt"]), - dateStart: moment(v.dateStart).format("DD-MM-YYYY"), - dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), + ..._.omit(v, ["dateStart", "dateEnd", "createdAt", "ProjectTaskFile"]), + dateStart: moment(v.dateStart).format("DD MMM YYYY"), + dateEnd: moment(v.dateEnd).format("DD MMM YYYY"), createdAt: moment(v.createdAt).format("DD-MM-YYYY HH:mm"), + files: v.ProjectTaskFile.map((tf: any) => ({ + name: tf.ProjectFile.name, + extension: tf.ProjectFile.extension + })) })) - // const dataFix = _.orderBy(formatData, 'createdAt', 'asc') allData = formatData } else if (kategori == "file") { diff --git a/src/app/api/mobile/project/task/file/[id]/route.ts b/src/app/api/mobile/project/task/file/[id]/route.ts new file mode 100644 index 0000000..b8b79a7 --- /dev/null +++ b/src/app/api/mobile/project/task/file/[id]/route.ts @@ -0,0 +1,198 @@ +import { DIR, funUploadFile, prisma } from "@/module/_global"; +import { funGetUserById } from "@/module/auth"; +import { createLogUserMobile } from "@/module/user"; +import { NextResponse } from "next/server"; + +// GET: daftar file yang terlampir pada ProjectTask +// [id] = ProjectTask.id +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { searchParams } = new URL(request.url); + const userMobile = searchParams.get("user"); + + const user = await funGetUserById({ id: String(userMobile) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const data = await prisma.projectTaskFile.findMany({ + where: { + idTask: id, + isActive: true, + }, + select: { + id: true, + ProjectFile: { + select: { + id: true, + name: true, + extension: true, + idStorage: true, + }, + }, + }, + orderBy: { createdAt: "asc" }, + }); + + const result = data.map((v) => ({ + id: v.id, // ProjectTaskFile.id — dipakai untuk DELETE + idFile: v.ProjectFile.id, // ProjectFile.id — dipakai untuk filter duplikat di picker + name: v.ProjectFile.name, + extension: v.ProjectFile.extension, + idStorage: v.ProjectFile.idStorage, + })); + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan file tugas", data: result }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan file tugas (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// POST: upload file baru ke ProjectTask +// Membuat ProjectFile baru lalu membuat ProjectTaskFile (junction) +// [id] = ProjectTask.id +export async function POST(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const body = await request.formData(); + const data = JSON.parse(body.get("data") as string); + + const user = await funGetUserById({ id: data.user }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const task = await prisma.projectTask.findUnique({ + where: { id }, + select: { id: true, idProject: true }, + }); + + if (!task) { + return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 }); + } + + const hasCekFile = body.has("file0"); + if (!hasCekFile) { + return NextResponse.json({ success: false, message: "Tidak ada file yang dikirim" }, { status: 200 }); + } + + body.delete("data"); + for (const [key] of body.entries()) { + if (!key.startsWith("file")) continue; + + const file = body.get(key) as File; + const fExt = file.name.split(".").pop(); + const fName = file.name.replace("." + fExt, ""); + + const upload = await funUploadFile({ file, dirId: DIR.project }); + if (!upload.success) continue; + + const projectFile = await prisma.projectFile.create({ + data: { + idProject: task.idProject, + name: fName, + extension: String(fExt), + idStorage: upload.data.id, + }, + select: { id: true }, + }); + + await prisma.projectTaskFile.create({ + data: { + idTask: id, + idFile: projectFile.id, + }, + }); + } + + await createLogUserMobile({ act: "CREATE", desc: "User menambah file pada tugas kegiatan", table: "projectTask", data: id, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil menambahkan file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal menambahkan file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// PATCH: link ProjectFile yang sudah ada ke ProjectTask +// Body: { user, idFile } — idFile = ProjectFile.id +// [id] = ProjectTask.id +export async function PATCH(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { user: userId, idFile } = await request.json(); + + const user = await funGetUserById({ id: String(userId) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const task = await prisma.projectTask.findUnique({ + where: { id }, + select: { id: true }, + }); + if (!task) { + return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 }); + } + + const file = await prisma.projectFile.findUnique({ + where: { id: idFile }, + select: { id: true }, + }); + if (!file) { + return NextResponse.json({ success: false, message: "File tidak ditemukan" }, { status: 200 }); + } + + // cek apakah sudah pernah di-link + const existing = await prisma.projectTaskFile.findFirst({ + where: { idTask: id, idFile, isActive: true }, + }); + if (existing) { + return NextResponse.json({ success: false, message: "File sudah terlampir pada tugas ini" }, { status: 200 }); + } + + await prisma.projectTaskFile.create({ + data: { idTask: id, idFile }, + }); + + await createLogUserMobile({ act: "CREATE", desc: "User melampirkan file kegiatan ke tugas", table: "projectTask", data: id, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil melampirkan file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal melampirkan file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// DELETE: hapus lampiran file dari ProjectTask (hapus junction record saja) +// [id] = ProjectTaskFile.id +export async function DELETE(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { user: userId } = await request.json(); + + const user = await funGetUserById({ id: String(userId) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const junction = await prisma.projectTaskFile.findUnique({ + where: { id }, + select: { id: true, idTask: true }, + }); + if (!junction) { + return NextResponse.json({ success: false, message: "Data tidak ditemukan" }, { status: 200 }); + } + + await prisma.projectTaskFile.delete({ where: { id } }); + + await createLogUserMobile({ act: "DELETE", desc: "User menghapus lampiran file dari tugas kegiatan", table: "projectTask", data: junction.idTask, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil menghapus lampiran file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal menghapus lampiran file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} diff --git a/src/app/api/mobile/task/[id]/route.ts b/src/app/api/mobile/task/[id]/route.ts index 7f285c6..4714ef5 100644 --- a/src/app/api/mobile/task/[id]/route.ts +++ b/src/app/api/mobile/task/[id]/route.ts @@ -74,6 +74,21 @@ export async function GET(request: Request, context: { params: { id: string } }) status: true, dateStart: true, dateEnd: true, + DivisionProjectTaskFile: { + where: { isActive: true }, + select: { + DivisionProjectFile: { + select: { + ContainerFileDivision: { + select: { + name: true, + extension: true, + }, + }, + }, + }, + }, + }, }, orderBy: { dateStart: 'asc' @@ -81,9 +96,13 @@ export async function GET(request: Request, context: { params: { id: string } }) }) const fix = dataProgress.map((v: any) => ({ - ..._.omit(v, ["dateStart", "dateEnd"]), - dateStart: moment(v.dateStart).format("DD-MM-YYYY"), - dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), + ..._.omit(v, ["dateStart", "dateEnd", "DivisionProjectTaskFile"]), + dateStart: moment(v.dateStart).format("DD MMM YYYY"), + dateEnd: moment(v.dateEnd).format("DD MMM YYYY"), + files: v.DivisionProjectTaskFile.map((tf: any) => ({ + name: tf.DivisionProjectFile.ContainerFileDivision.name, + extension: tf.DivisionProjectFile.ContainerFileDivision.extension, + })), })) allData = fix diff --git a/src/app/api/mobile/task/tugas/file/[id]/route.ts b/src/app/api/mobile/task/tugas/file/[id]/route.ts new file mode 100644 index 0000000..b9f7365 --- /dev/null +++ b/src/app/api/mobile/task/tugas/file/[id]/route.ts @@ -0,0 +1,210 @@ +import { DIR, funUploadFile, prisma } from "@/module/_global"; +import { funGetUserById } from "@/module/auth"; +import { createLogUserMobile } from "@/module/user"; +import { NextResponse } from "next/server"; + +// GET: daftar file yang terlampir pada DivisionProjectTask +// [id] = DivisionProjectTask.id +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { searchParams } = new URL(request.url); + const userMobile = searchParams.get("user"); + + const user = await funGetUserById({ id: String(userMobile) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const data = await prisma.divisionProjectTaskFile.findMany({ + where: { + idTask: id, + isActive: true, + }, + select: { + id: true, + DivisionProjectFile: { + select: { + id: true, + ContainerFileDivision: { + select: { + name: true, + extension: true, + idStorage: true, + }, + }, + }, + }, + }, + orderBy: { createdAt: "asc" }, + }); + + const result = data.map((v) => ({ + id: v.id, + idFile: v.DivisionProjectFile.id, + name: v.DivisionProjectFile.ContainerFileDivision.name, + extension: v.DivisionProjectFile.ContainerFileDivision.extension, + idStorage: v.DivisionProjectFile.ContainerFileDivision.idStorage, + })); + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan file tugas", data: result }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan file tugas (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// POST: upload file baru ke DivisionProjectTask +// [id] = DivisionProjectTask.id +export async function POST(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const body = await request.formData(); + const data = JSON.parse(body.get("data") as string); + + const user = await funGetUserById({ id: data.user }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const task = await prisma.divisionProjectTask.findUnique({ + where: { id }, + select: { id: true, idProject: true, idDivision: true }, + }); + + if (!task) { + return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 }); + } + + const hasCekFile = body.has("file0"); + if (!hasCekFile) { + return NextResponse.json({ success: false, message: "Tidak ada file yang dikirim" }, { status: 200 }); + } + + body.delete("data"); + for (const [key] of body.entries()) { + if (!key.startsWith("file")) continue; + + const file = body.get(key) as File; + const fExt = file.name.split(".").pop(); + const fName = file.name.replace("." + fExt, ""); + + const upload = await funUploadFile({ file, dirId: DIR.task }); + if (!upload.success) continue; + + const container = await prisma.containerFileDivision.create({ + data: { + idDivision: task.idDivision, + name: fName, + extension: String(fExt), + idStorage: upload.data.id, + }, + select: { id: true }, + }); + + const divFile = await prisma.divisionProjectFile.create({ + data: { + idProject: task.idProject, + idDivision: task.idDivision, + idFile: container.id, + createdBy: user.id, + }, + select: { id: true }, + }); + + await prisma.divisionProjectTaskFile.create({ + data: { + idTask: id, + idFile: divFile.id, + }, + }); + } + + await createLogUserMobile({ act: "CREATE", desc: "User menambah file pada tugas divisi", table: "divisionProjectTask", data: id, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil menambahkan file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal menambahkan file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// PATCH: link DivisionProjectFile yang sudah ada ke DivisionProjectTask +// Body: { user, idFile } — idFile = DivisionProjectFile.id +// [id] = DivisionProjectTask.id +export async function PATCH(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { user: userId, idFile } = await request.json(); + + const user = await funGetUserById({ id: String(userId) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const task = await prisma.divisionProjectTask.findUnique({ + where: { id }, + select: { id: true }, + }); + if (!task) { + return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 }); + } + + const file = await prisma.divisionProjectFile.findUnique({ + where: { id: idFile }, + select: { id: true }, + }); + if (!file) { + return NextResponse.json({ success: false, message: "File tidak ditemukan" }, { status: 200 }); + } + + const existing = await prisma.divisionProjectTaskFile.findFirst({ + where: { idTask: id, idFile, isActive: true }, + }); + if (existing) { + return NextResponse.json({ success: false, message: "File sudah terlampir pada tugas ini" }, { status: 200 }); + } + + await prisma.divisionProjectTaskFile.create({ + data: { idTask: id, idFile }, + }); + + await createLogUserMobile({ act: "CREATE", desc: "User melampirkan file divisi ke tugas", table: "divisionProjectTask", data: id, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil melampirkan file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal melampirkan file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +} + +// DELETE: hapus lampiran file dari DivisionProjectTask (hapus junction record saja) +// [id] = DivisionProjectTaskFile.id +export async function DELETE(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { user: userId } = await request.json(); + + const user = await funGetUserById({ id: String(userId) }); + if (!user.id || user.id === "null") { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 }); + } + + const junction = await prisma.divisionProjectTaskFile.findUnique({ + where: { id }, + select: { id: true, idTask: true }, + }); + if (!junction) { + return NextResponse.json({ success: false, message: "Data tidak ditemukan" }, { status: 200 }); + } + + await prisma.divisionProjectTaskFile.delete({ where: { id } }); + + await createLogUserMobile({ act: "DELETE", desc: "User menghapus lampiran file dari tugas divisi", table: "divisionProjectTask", data: junction.idTask, user: user.id }); + + return NextResponse.json({ success: true, message: "Berhasil menghapus lampiran file" }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal menghapus lampiran file (error: 500)", reason: (error as Error).message }, { status: 500 }); + } +}