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 {