amalia/06-mei-26 #43
214
generate_erd.py
Normal file
214
generate_erd.py
Normal file
@@ -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)
|
||||
35
prisma/migrations/20260505093957_add_task_file/migration.sql
Normal file
35
prisma/migrations/20260505093957_add_task_file/migration.sql
Normal file
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
198
src/app/api/mobile/project/task/file/[id]/route.ts
Normal file
198
src/app/api/mobile/project/task/file/[id]/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
210
src/app/api/mobile/task/tugas/file/[id]/route.ts
Normal file
210
src/app/api/mobile/task/tugas/file/[id]/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user