Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c4d6bce6e | |||
| 9db9462faf | |||
| 05a8fa0c47 |
@@ -3,37 +3,3 @@
|
||||
Docker images are built via `.github/workflows/publish.yml` and pushed to GHCR (`ghcr.io`). Portainer redeploys via `.github/workflows/re-pull.yml`. Supports `dev`, `stg`, and `prod` stacks.
|
||||
|
||||
The Dockerfile uses a two-stage build: Bun builder → Bun runner (non-root user, port 3000).
|
||||
|
||||
## Git Remote Structure
|
||||
|
||||
| Remote | URL | Purpose |
|
||||
|--------|-----|---------|
|
||||
| `origin` | wibugit.wibudev.com/wibu/sistem-desa-mandiri | Repo kerja tim |
|
||||
| `build` | github.com/bipprojectbali/desa-plus | Repo deployment (trigger CI/CD) |
|
||||
|
||||
**Branch mapping:**
|
||||
- `origin/staging` — branch integrasi tim (bukan deployment target)
|
||||
- `build/stg` — branch deployment stg (trigger publish image + Portainer repull)
|
||||
- `build/prod` — branch deployment prod
|
||||
- `build/dev` — branch deployment dev
|
||||
|
||||
## Deploy to STG Flow
|
||||
|
||||
Cukup jalankan MCP `deploy-stg` — handles otomatis: cek migrasi → bump version → commit → push ke `build/stg` → trigger publish workflow (`ref: stg`) → tunggu selesai → trigger repull Portainer → verify version via `BASE_URL${VERSION_PATH}`.
|
||||
|
||||
> `origin` tidak punya branch `stg` (hanya `staging`). "stg" selalu merujuk ke `build/stg`.
|
||||
|
||||
## MCP `deploy-stg`
|
||||
|
||||
Lokasi: `.mcp/deploy-stg/server.ts`. Berkomunikasi langsung dengan GitHub REST API (tidak butuh `gh` CLI), hanya perlu `git` & `prisma` lokal.
|
||||
|
||||
**Env vars** (di `.mcp.json` atau `.env`):
|
||||
- `GH_TOKEN` — PAT dengan scope `repo` + `workflow` untuk trigger Actions
|
||||
- `GH_URL` — repo build target, format `owner/repo` atau full URL
|
||||
- `BASE_URL` — base URL stg untuk verifikasi versi
|
||||
- `VERSION_PATH` — endpoint cek versi (default `/api/version-app`)
|
||||
- `STACK_NAME` — nama stack Portainer
|
||||
|
||||
**Tools:** `deploy`, `publish`, `repull`, `run_status`, `check_version`.
|
||||
|
||||
**Penting:** workflow `publish.yml` & `re-pull.yml` di-trigger dengan `ref: stg` agar `actions/checkout@v4` checkout dari branch `stg`, bukan default branch (`main`).
|
||||
|
||||
@@ -10,15 +10,3 @@ Copy `.env.example` to `.env`. Required variables:
|
||||
| `WS_APIKEY` | WebSocket/file storage API key |
|
||||
| `WIBU_REALTIME_KEY` | Real-time communication |
|
||||
| `FCM_KEY` | Firebase Cloud Messaging |
|
||||
|
||||
## Deployment (MCP `deploy-stg`)
|
||||
|
||||
Diisi di `.env` lokal (jangan commit `GH_TOKEN`). `.mcp.json` me-reference via `${GH_TOKEN}`.
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `GH_TOKEN` | GitHub PAT dengan scope `repo` + `workflow` |
|
||||
| `GH_URL` | Repo build target (`owner/repo` atau full URL) |
|
||||
| `BASE_URL` | Base URL deployment stg (untuk verifikasi versi) |
|
||||
| `VERSION_PATH` | Endpoint cek versi (default `/api/version-app`) |
|
||||
| `STACK_NAME` | Nama stack di Portainer |
|
||||
|
||||
@@ -48,12 +48,6 @@ WS_APIKEY="your-websocket-api-key"
|
||||
# API key untuk akses endpoint /api/monitoring (header: x-api-key)
|
||||
MONITORING_API_KEY="your-monitoring-api-key"
|
||||
|
||||
# ===========================================
|
||||
# AI API
|
||||
# ===========================================
|
||||
# API key untuk akses endpoint /api/ai/* (header: x-api-key)
|
||||
AI_API_KEY="your-ai-api-key"
|
||||
|
||||
# ===========================================
|
||||
# APPLICATION SETTINGS
|
||||
# ===========================================
|
||||
|
||||
@@ -5,13 +5,9 @@
|
||||
"command": "bun",
|
||||
"args": ["run", ".mcp/deploy-stg/server.ts"],
|
||||
"env": {
|
||||
"GH_TOKEN": "${GH_TOKEN}",
|
||||
"GH_URL": "bipprojectbali/desa-plus",
|
||||
"BASE_URL": "https://desa-plus-stg.wibudev.com",
|
||||
"VERSION_PATH": "/api/version-app",
|
||||
"STACK_NAME": "desa-plus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,127 +4,24 @@ import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { execFileSync } from "child_process";
|
||||
import { execFileSync, execSync } from "child_process";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
||||
const REPO = "bipprojectbali/desa-plus";
|
||||
const STACK_ENV = "stg";
|
||||
const BASE_URL = process.env.BASE_URL ?? "";
|
||||
const VERSION_PATH = process.env.VERSION_PATH ?? "/api/version-app";
|
||||
const DEFAULT_STACK_NAME = process.env.STACK_NAME ?? "";
|
||||
const GH_TOKEN = process.env.GH_TOKEN ?? "";
|
||||
|
||||
const GH_URL_RAW = process.env.GH_URL ?? "";
|
||||
// support both "owner/repo" and "https://github.com/owner/repo" formats
|
||||
const REPO = GH_URL_RAW.startsWith("http")
|
||||
? GH_URL_RAW.replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "")
|
||||
: GH_URL_RAW;
|
||||
const GH = (args: string[]) =>
|
||||
execFileSync("gh", args, { encoding: "utf-8", cwd: PROJECT_ROOT }).trim();
|
||||
|
||||
const GIT = (args: string[]) =>
|
||||
execFileSync("git", args, { encoding: "utf-8", cwd: PROJECT_ROOT }).trim();
|
||||
|
||||
// --- GitHub API client (no gh CLI) ---
|
||||
|
||||
const GH_API = "https://api.github.com";
|
||||
|
||||
type WorkflowRun = {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
conclusion: string | null;
|
||||
html_url: string;
|
||||
created_at: string;
|
||||
run_started_at: string;
|
||||
};
|
||||
|
||||
async function ghFetch<T = unknown>(
|
||||
pathname: string,
|
||||
init: RequestInit = {}
|
||||
): Promise<T> {
|
||||
if (!GH_TOKEN) throw new Error("GH_TOKEN tidak di-set.");
|
||||
if (!REPO) throw new Error("GH_URL tidak di-set.");
|
||||
|
||||
const res = await fetch(`${GH_API}${pathname}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${GH_TOKEN}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
...(init.body ? { "Content-Type": "application/json" } : {}),
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`GitHub API ${res.status} ${res.statusText}: ${body}`);
|
||||
}
|
||||
|
||||
// 204 No Content (e.g. workflow dispatch)
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
async function triggerWorkflow(
|
||||
workflow: string,
|
||||
ref: string,
|
||||
inputs: Record<string, string>
|
||||
): Promise<void> {
|
||||
await ghFetch(`/repos/${REPO}/actions/workflows/${workflow}/dispatches`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ref, inputs }),
|
||||
});
|
||||
}
|
||||
|
||||
async function getLatestRun(workflow: string): Promise<WorkflowRun> {
|
||||
const data = await ghFetch<{ workflow_runs: WorkflowRun[] }>(
|
||||
`/repos/${REPO}/actions/workflows/${workflow}/runs?per_page=1`
|
||||
);
|
||||
if (!data.workflow_runs?.length) {
|
||||
throw new Error(`Tidak ada run untuk workflow ${workflow}.`);
|
||||
}
|
||||
return data.workflow_runs[0];
|
||||
}
|
||||
|
||||
async function listRuns(
|
||||
workflow: string | "all",
|
||||
limit: number
|
||||
): Promise<WorkflowRun[]> {
|
||||
const url =
|
||||
workflow === "all"
|
||||
? `/repos/${REPO}/actions/runs?per_page=${limit}`
|
||||
: `/repos/${REPO}/actions/workflows/${workflow}/runs?per_page=${limit}`;
|
||||
const data = await ghFetch<{ workflow_runs: WorkflowRun[] }>(url);
|
||||
return data.workflow_runs ?? [];
|
||||
}
|
||||
|
||||
async function waitForRun(
|
||||
runId: number,
|
||||
timeoutMs = 30 * 60 * 1000
|
||||
): Promise<WorkflowRun> {
|
||||
const interval = 10_000;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const run = await ghFetch<WorkflowRun>(
|
||||
`/repos/${REPO}/actions/runs/${runId}`
|
||||
);
|
||||
if (run.status === "completed") {
|
||||
if (run.conclusion !== "success") {
|
||||
throw new Error(
|
||||
`Run ${runId} selesai dengan conclusion: ${run.conclusion}`
|
||||
);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, interval));
|
||||
}
|
||||
throw new Error(`Timeout menunggu run ${runId}.`);
|
||||
}
|
||||
|
||||
// --- version helpers ---
|
||||
|
||||
function bumpVersion(version: string, type: "patch" | "minor" | "major"): string {
|
||||
@@ -151,7 +48,7 @@ function applyVersionBump(newVersion: string): void {
|
||||
async function waitForDeployedVersion(expected: string, timeoutMs = 5 * 60 * 1000): Promise<string> {
|
||||
if (!BASE_URL) return "BASE_URL tidak di-set, skip cek versi stg.";
|
||||
|
||||
const url = `${BASE_URL}${VERSION_PATH}`;
|
||||
const url = `${BASE_URL}/api/version-app`;
|
||||
const interval = 15_000;
|
||||
const maxAttempts = Math.ceil(timeoutMs / interval);
|
||||
let last = "";
|
||||
@@ -302,26 +199,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const currentBranch = GIT(["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||
GIT(["push", "build", `${currentBranch}:stg`, "--force"]);
|
||||
|
||||
// 4. Trigger publish workflow
|
||||
await triggerWorkflow("publish.yml", STACK_ENV, {
|
||||
stack_env: STACK_ENV,
|
||||
tag: newVersion,
|
||||
});
|
||||
// 4. Trigger publish
|
||||
GH([
|
||||
"workflow", "run", "publish.yml",
|
||||
"--repo", REPO,
|
||||
"--field", `stack_env=${STACK_ENV}`,
|
||||
"--field", `tag=${newVersion}`,
|
||||
]);
|
||||
await new Promise((r) => setTimeout(r, 4000));
|
||||
|
||||
const publishRun = await getLatestRun("publish.yml");
|
||||
const publishRunId = GH([
|
||||
"run", "list", "--repo", REPO,
|
||||
"--workflow", "publish.yml",
|
||||
"--limit", "1",
|
||||
"--json", "databaseId",
|
||||
"--jq", ".[0].databaseId",
|
||||
]);
|
||||
const publishUrl = GH([
|
||||
"run", "list", "--repo", REPO,
|
||||
"--workflow", "publish.yml",
|
||||
"--limit", "1",
|
||||
"--json", "url",
|
||||
"--jq", ".[0].url",
|
||||
]);
|
||||
|
||||
// 5. Wait for publish to finish
|
||||
await waitForRun(publishRun.id);
|
||||
execSync(`gh run watch ${publishRunId} --repo ${REPO} --exit-status`, {
|
||||
encoding: "utf-8",
|
||||
cwd: PROJECT_ROOT,
|
||||
timeout: 30 * 60 * 1000,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
// 6. Trigger repull
|
||||
await triggerWorkflow("re-pull.yml", STACK_ENV, {
|
||||
stack_name,
|
||||
stack_env: STACK_ENV,
|
||||
});
|
||||
GH([
|
||||
"workflow", "run", "re-pull.yml",
|
||||
"--repo", REPO,
|
||||
"--field", `stack_name=${stack_name}`,
|
||||
"--field", `stack_env=${STACK_ENV}`,
|
||||
]);
|
||||
await new Promise((r) => setTimeout(r, 4000));
|
||||
|
||||
const repullRun = await getLatestRun("re-pull.yml");
|
||||
const repullUrl = GH([
|
||||
"run", "list", "--repo", REPO,
|
||||
"--workflow", "re-pull.yml",
|
||||
"--limit", "1",
|
||||
"--json", "url",
|
||||
"--jq", ".[0].url",
|
||||
]);
|
||||
|
||||
// 7. Wait for repull, then verify version
|
||||
await new Promise((r) => setTimeout(r, 30_000));
|
||||
@@ -335,8 +260,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
type: "text",
|
||||
text: [
|
||||
`Deploy selesai: ${stack_name}-${STACK_ENV} @ ${newVersion} (dari ${oldVersion})`,
|
||||
`Publish run : ${publishRun.html_url}`,
|
||||
`Repull run : ${repullRun.html_url}`,
|
||||
`Publish run : ${publishUrl}`,
|
||||
`Repull run : ${repullUrl}`,
|
||||
``,
|
||||
`Versi lokal : ${localVer}`,
|
||||
versionCheck,
|
||||
@@ -350,16 +275,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (name === "publish") {
|
||||
const tag = readPkgVersion();
|
||||
|
||||
await triggerWorkflow("publish.yml", STACK_ENV, {
|
||||
stack_env: STACK_ENV,
|
||||
tag,
|
||||
});
|
||||
GH([
|
||||
"workflow", "run", "publish.yml",
|
||||
"--repo", REPO,
|
||||
"--field", `stack_env=${STACK_ENV}`,
|
||||
"--field", `tag=${tag}`,
|
||||
]);
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
const run = await getLatestRun("publish.yml");
|
||||
const runUrl = GH([
|
||||
"run", "list", "--repo", REPO,
|
||||
"--workflow", "publish.yml",
|
||||
"--limit", "1",
|
||||
"--json", "url",
|
||||
"--jq", ".[0].url",
|
||||
]);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Publish triggered: ${STACK_ENV}-${tag}\nRun: ${run.html_url}` }],
|
||||
content: [{ type: "text", text: `Publish triggered: ${STACK_ENV}-${tag}\nRun: ${runUrl}` }],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -369,16 +302,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const stack_name = _sn || DEFAULT_STACK_NAME;
|
||||
if (!stack_name) throw new Error("stack_name tidak diisi dan env STACK_NAME kosong.");
|
||||
|
||||
await triggerWorkflow("re-pull.yml", STACK_ENV, {
|
||||
stack_name,
|
||||
stack_env: STACK_ENV,
|
||||
});
|
||||
GH([
|
||||
"workflow", "run", "re-pull.yml",
|
||||
"--repo", REPO,
|
||||
"--field", `stack_name=${stack_name}`,
|
||||
"--field", `stack_env=${STACK_ENV}`,
|
||||
]);
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
const run = await getLatestRun("re-pull.yml");
|
||||
const runUrl = GH([
|
||||
"run", "list", "--repo", REPO,
|
||||
"--workflow", "re-pull.yml",
|
||||
"--limit", "1",
|
||||
"--json", "url",
|
||||
"--jq", ".[0].url",
|
||||
]);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Repull triggered: ${stack_name}-${STACK_ENV}\nRun: ${run.html_url}` }],
|
||||
content: [{ type: "text", text: `Repull triggered: ${stack_name}-${STACK_ENV}\nRun: ${runUrl}` }],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,14 +329,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
workflow?: string;
|
||||
limit?: number;
|
||||
};
|
||||
const workflowArgs = workflow === "all" ? [] : ["--workflow", workflow];
|
||||
|
||||
const runs = await listRuns(workflow, limit);
|
||||
const output = runs
|
||||
.map(
|
||||
(r) =>
|
||||
`[${r.status}/${r.conclusion ?? "-"}] ${r.name} — ${r.run_started_at}\n ${r.html_url}`
|
||||
)
|
||||
.join("\n");
|
||||
const output = GH([
|
||||
"run", "list",
|
||||
"--repo", REPO,
|
||||
...workflowArgs,
|
||||
"--limit", String(limit),
|
||||
"--json", "workflowName,status,conclusion,startedAt,url,databaseId",
|
||||
"--jq",
|
||||
'.[] | "[\(.status)/\(.conclusion // "-")] \(.workflowName) — \(.startedAt)\n \(.url)"',
|
||||
]);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: output || "Tidak ada run ditemukan." }],
|
||||
@@ -409,7 +353,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
|
||||
if (BASE_URL) {
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}${VERSION_PATH}`);
|
||||
const res = await fetch(`${BASE_URL}/api/version-app`);
|
||||
const data = (await res.json()) as { version?: string };
|
||||
stgVersion = data.version ?? "?";
|
||||
} catch (e) {
|
||||
@@ -427,7 +371,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
type: "text",
|
||||
text: [
|
||||
`Lokal (package.json) : ${localVersion}`,
|
||||
`Stg (${VERSION_PATH}): ${stgVersion}`,
|
||||
`Stg (/api/version-app): ${stgVersion}`,
|
||||
`Status : ${match}`,
|
||||
].join("\n"),
|
||||
},
|
||||
|
||||
214
generate_erd.py
214
generate_erd.py
@@ -1,214 +0,0 @@
|
||||
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)
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "sistem-desa-mandiri",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --experimental-https",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"claude": "set -a && source .env && set +a && claude"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "npx tsx prisma/seed.ts"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1,35 +0,0 @@
|
||||
-- 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;
|
||||
@@ -1,48 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "isApprover" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProjectTaskApproval" (
|
||||
"id" TEXT NOT NULL,
|
||||
"idTask" TEXT NOT NULL,
|
||||
"idUser" TEXT NOT NULL,
|
||||
"idApprover" TEXT,
|
||||
"status" INTEGER NOT NULL DEFAULT 0,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProjectTaskApproval_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DivisionProjectTaskApproval" (
|
||||
"id" TEXT NOT NULL,
|
||||
"idTask" TEXT NOT NULL,
|
||||
"idUser" TEXT NOT NULL,
|
||||
"idApprover" TEXT,
|
||||
"status" INTEGER NOT NULL DEFAULT 0,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DivisionProjectTaskApproval_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectTaskApproval" ADD CONSTRAINT "ProjectTaskApproval_idTask_fkey" FOREIGN KEY ("idTask") REFERENCES "ProjectTask"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectTaskApproval" ADD CONSTRAINT "ProjectTaskApproval_idUser_fkey" FOREIGN KEY ("idUser") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectTaskApproval" ADD CONSTRAINT "ProjectTaskApproval_idApprover_fkey" FOREIGN KEY ("idApprover") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DivisionProjectTaskApproval" ADD CONSTRAINT "DivisionProjectTaskApproval_idTask_fkey" FOREIGN KEY ("idTask") REFERENCES "DivisionProjectTask"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DivisionProjectTaskApproval" ADD CONSTRAINT "DivisionProjectTaskApproval_idUser_fkey" FOREIGN KEY ("idUser") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DivisionProjectTaskApproval" ADD CONSTRAINT "DivisionProjectTaskApproval_idApprover_fkey" FOREIGN KEY ("idApprover") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1,14 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiKey" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -1 +0,0 @@
|
||||
-- This is an empty migration.
|
||||
@@ -109,7 +109,6 @@ model User {
|
||||
img String?
|
||||
isFirstLogin Boolean @default(true)
|
||||
isWithoutOTP Boolean @default(false)
|
||||
isApprover Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -126,17 +125,13 @@ model User {
|
||||
DivisionDocumentFolderFile DivisionDocumentFolderFile[]
|
||||
DivisionCalendar DivisionCalendar[]
|
||||
DivisionCalendarMember DivisionCalendarMember[]
|
||||
Notifications Notifications[] @relation("UserToUser")
|
||||
Notifications2 Notifications[] @relation("UserFromUser")
|
||||
Subscribe Subscribe?
|
||||
Discussion Discussion[]
|
||||
DiscussionMember DiscussionMember[]
|
||||
DiscussionComment DiscussionComment[]
|
||||
TokenDeviceUser TokenDeviceUser[]
|
||||
ProjectTaskApprovalSubmitted ProjectTaskApproval[] @relation("ApprovalSubmitter")
|
||||
ProjectTaskApprovalHandled ProjectTaskApproval[] @relation("ApprovalApprover")
|
||||
DivisionProjectTaskApprovalSubmitted DivisionProjectTaskApproval[] @relation("DivApprovalSubmitter")
|
||||
DivisionProjectTaskApprovalHandled DivisionProjectTaskApproval[] @relation("DivApprovalApprover")
|
||||
Notifications Notifications[] @relation("UserToUser")
|
||||
Notifications2 Notifications[] @relation("UserFromUser")
|
||||
Subscribe Subscribe?
|
||||
Discussion Discussion[]
|
||||
DiscussionMember DiscussionMember[]
|
||||
DiscussionComment DiscussionComment[]
|
||||
TokenDeviceUser TokenDeviceUser[]
|
||||
}
|
||||
|
||||
model TokenDeviceUser {
|
||||
@@ -237,16 +232,15 @@ 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
|
||||
ProjectTaskFile ProjectTaskFile[]
|
||||
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
|
||||
}
|
||||
|
||||
model ProjectLink {
|
||||
@@ -265,27 +259,14 @@ model ProjectTask {
|
||||
idProject String
|
||||
title String
|
||||
desc String?
|
||||
status Int @default(0) // 0 = todo, 1 = done, 2 = waiting_approval
|
||||
notifikasi Boolean @default(false)
|
||||
dateStart DateTime @db.Date
|
||||
dateEnd DateTime @db.Date
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ProjectTaskDetail ProjectTaskDetail[]
|
||||
ProjectTaskFile ProjectTaskFile[]
|
||||
ProjectTaskApproval ProjectTaskApproval[]
|
||||
}
|
||||
|
||||
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
|
||||
status Int @default(0) // 0 = todo, 1 = done
|
||||
notifikasi Boolean @default(false)
|
||||
dateStart DateTime @db.Date
|
||||
dateEnd DateTime @db.Date
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ProjectTaskDetail ProjectTaskDetail[]
|
||||
}
|
||||
|
||||
model ProjectTaskDetail {
|
||||
@@ -379,16 +360,14 @@ model DivisionProjectTask {
|
||||
idProject String
|
||||
title String
|
||||
desc String? @db.Text
|
||||
status Int @default(0) // 0 = todo, 1 = done, 2 = waiting_approval
|
||||
notifikasi Boolean @default(false)
|
||||
dateStart DateTime @db.Date
|
||||
dateEnd DateTime @db.Date
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
DivisionProjectTaskDetail DivisionProjectTaskDetail[]
|
||||
DivisionProjectTaskFile DivisionProjectTaskFile[]
|
||||
DivisionProjectTaskApproval DivisionProjectTaskApproval[]
|
||||
status Int @default(0) // 0 = todo, 1 = done
|
||||
notifikasi Boolean @default(false)
|
||||
dateStart DateTime @db.Date
|
||||
dateEnd DateTime @db.Date
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
DivisionProjectTaskDetail DivisionProjectTaskDetail[]
|
||||
}
|
||||
|
||||
model DivisionProjectTaskDetail {
|
||||
@@ -418,30 +397,18 @@ 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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
model DivisionDisscussion {
|
||||
@@ -701,40 +668,3 @@ model Setting{
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ProjectTaskApproval {
|
||||
id String @id @default(cuid())
|
||||
ProjectTask ProjectTask @relation(fields: [idTask], references: [id])
|
||||
idTask String
|
||||
Submitter User @relation("ApprovalSubmitter", fields: [idUser], references: [id])
|
||||
idUser String
|
||||
Approver User? @relation("ApprovalApprover", fields: [idApprover], references: [id])
|
||||
idApprover String?
|
||||
status Int @default(0) // 0 = pending, 1 = approved, 2 = rejected
|
||||
note String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model DivisionProjectTaskApproval {
|
||||
id String @id @default(cuid())
|
||||
DivisionProjectTask DivisionProjectTask @relation(fields: [idTask], references: [id])
|
||||
idTask String
|
||||
Submitter User @relation("DivApprovalSubmitter", fields: [idUser], references: [id])
|
||||
idUser String
|
||||
Approver User? @relation("DivApprovalApprover", fields: [idApprover], references: [id])
|
||||
idApprover String?
|
||||
status Int @default(0) // 0 = pending, 1 = approved, 2 = rejected
|
||||
note String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
key String @unique
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
95
src/app/api/ai/announcement/[id]/route.ts
Normal file
95
src/app/api/ai/announcement/[id]/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ONE PENGUMUMAN, UNTUK TAMPIL DETAIL PENGUMUMAN
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
|
||||
const data = await prisma.announcement.count({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (data == 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Gagal mendapatkan pengumuman, data tidak ditemukan",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const announcement = await prisma.announcement.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!announcement) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Gagal mendapatkan pengumuman, data tidak ditemukan",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
let dataFix = { ...announcement, member: {} };
|
||||
|
||||
const announcementMember = await prisma.announcementMember.findMany({
|
||||
where: {
|
||||
idAnnouncement: id,
|
||||
},
|
||||
select: {
|
||||
idGroup: true,
|
||||
idDivision: true,
|
||||
Group: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
Division: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const formatMember = announcementMember.map((v: any) => ({
|
||||
..._.omit(v, ["Group", "Division"]),
|
||||
idGroup: v.idGroup,
|
||||
idDivision: v.idDivision,
|
||||
group: v.Group.name,
|
||||
division: v.Division.name
|
||||
}))
|
||||
|
||||
dataFix.member = formatMember
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Berhasil mendapatkan pengumuman",
|
||||
data: dataFix,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan pengumuman, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
52
src/app/api/ai/announcement/route.ts
Normal file
52
src/app/api/ai/announcement/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
|
||||
|
||||
// GET ALL PENGUMUMAN
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const judul = searchParams.get('search');
|
||||
const page = searchParams.get('page');
|
||||
const get = searchParams.get('get');
|
||||
const villageId = searchParams.get('desa');
|
||||
const active = searchParams.get('active');
|
||||
|
||||
let getFix = 0;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
|
||||
let kondisi: any = {
|
||||
idVillage: String(villageId),
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
title: {
|
||||
contains: (judul == undefined || judul == null) ? "" : judul,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.announcement.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan pengumuman", data, }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan pengumuman, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
21
src/app/api/ai/banner/[id]/route.ts
Normal file
21
src/app/api/ai/banner/[id]/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ONE BANNER
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
|
||||
const data = await prisma.bannerImage.findUnique({
|
||||
where: {
|
||||
id: String(id)
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan banner", data }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan banner, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
48
src/app/api/ai/banner/route.ts
Normal file
48
src/app/api/ai/banner/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL BANNER
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const judul = searchParams.get('search');
|
||||
const page = searchParams.get('page');
|
||||
const get = searchParams.get('get');
|
||||
const villageId = searchParams.get('desa');
|
||||
const active = searchParams.get('active');
|
||||
|
||||
let getFix = 0;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
idVillage: String(villageId),
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
title: {
|
||||
contains: (judul == undefined || judul == null) ? "" : judul,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.bannerImage.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan banner", data }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan data banner, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
99
src/app/api/ai/calendar/[id]/route.ts
Normal file
99
src/app/api/ai/calendar/[id]/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET ONE CALENDER BY ID KALENDER REMINDER
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params
|
||||
|
||||
const cek = await prisma.divisionCalendarReminder.count({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
})
|
||||
|
||||
if (cek == 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Gagal mendapatkan acara, data tidak ditemukan",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const data: any = await prisma.divisionCalendarReminder.findUnique({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
timeStart: true,
|
||||
dateStart: true,
|
||||
timeEnd: true,
|
||||
createdAt: true,
|
||||
DivisionCalendar: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
linkMeet: true,
|
||||
repeatEventTyper: true,
|
||||
repeatValue: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const { DivisionCalendar, ...dataCalender } = data
|
||||
const timeStart = moment.utc(dataCalender?.timeStart).format("HH:mm")
|
||||
const timeEnd = moment.utc(dataCalender?.timeEnd).format("HH:mm")
|
||||
const idCalendar = data?.DivisionCalendar.id
|
||||
const title = data?.DivisionCalendar?.title
|
||||
const desc = data?.DivisionCalendar?.desc
|
||||
const linkMeet = data?.DivisionCalendar?.linkMeet
|
||||
const repeatEventTyper = data?.DivisionCalendar?.repeatEventTyper
|
||||
const repeatValue = data?.DivisionCalendar?.repeatValue
|
||||
|
||||
|
||||
const result = { ...dataCalender, timeStart, timeEnd, title, desc, linkMeet, repeatEventTyper, repeatValue }
|
||||
|
||||
|
||||
const member = await prisma.divisionCalendarMember.findMany({
|
||||
where: {
|
||||
idCalendar
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
img: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const fixMember = member.map((v: any) => ({
|
||||
..._.omit(v, ["User"]),
|
||||
name: v.User.name,
|
||||
email: v.User.email,
|
||||
img: v.User.img
|
||||
}))
|
||||
|
||||
|
||||
const dataFix = {
|
||||
...result,
|
||||
member: fixMember,
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan kalender", data: dataFix }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kalender, data tidak ditemukan (error: 500)", }, { status: 500 });
|
||||
}
|
||||
}
|
||||
149
src/app/api/ai/calendar/route.ts
Normal file
149
src/app/api/ai/calendar/route.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
//GET ALL CALENDER
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idDivision = searchParams.get("division");
|
||||
const isDate = searchParams.get("date")
|
||||
const villageId = searchParams.get("desa")
|
||||
const active = searchParams.get("active")
|
||||
const search = searchParams.get("search")
|
||||
const page = searchParams.get("page")
|
||||
const get = searchParams.get("get")
|
||||
|
||||
let getFix = 0;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
|
||||
let kondisi: any = {}
|
||||
|
||||
if (idDivision != "" && idDivision != null && idDivision != undefined) {
|
||||
if (isDate != null && isDate != undefined && isDate != "") {
|
||||
kondisi = {
|
||||
idDivision: String(idDivision),
|
||||
dateStart: new Date(String(isDate)),
|
||||
DivisionCalendar: {
|
||||
title: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kondisi = {
|
||||
idDivision: String(idDivision),
|
||||
DivisionCalendar: {
|
||||
title: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isDate != null && isDate != undefined && isDate != "") {
|
||||
kondisi = {
|
||||
dateStart: new Date(String(isDate)),
|
||||
DivisionCalendar: {
|
||||
title: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kondisi = {
|
||||
DivisionCalendar: {
|
||||
title: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
isActive: (active == "false" || active == undefined) ? false : true,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.divisionCalendarReminder.findMany({
|
||||
where: kondisi,
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
select: {
|
||||
id: true,
|
||||
dateStart: true,
|
||||
timeStart: true,
|
||||
timeEnd: true,
|
||||
createdAt: true,
|
||||
DivisionCalendar: {
|
||||
select: {
|
||||
isActive: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
dateStart: 'asc'
|
||||
},
|
||||
{
|
||||
timeStart: 'asc'
|
||||
},
|
||||
{
|
||||
timeEnd: 'asc'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const allOmit = data.map((v: any) => ({
|
||||
..._.omit(v, ["DivisionCalendar", "User"]),
|
||||
title: v.DivisionCalendar.title,
|
||||
desc: v.DivisionCalendar.desc,
|
||||
createdBy: v.DivisionCalendar.User.name,
|
||||
isActive: v.DivisionCalendar.isActive,
|
||||
timeStart: moment.utc(v.timeStart).format('HH:mm'),
|
||||
timeEnd: moment.utc(v.timeEnd).format('HH:mm')
|
||||
}))
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan kalender", data: allOmit }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kalender, data tidak ditemukan (error: 500)" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
117
src/app/api/ai/discussion-general/[id]/route.ts
Normal file
117
src/app/api/ai/discussion-general/[id]/route.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ONE DETAIL DISKUSI UMUM
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
let dataFix
|
||||
const { id } = context.params
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kategori = searchParams.get("cat");
|
||||
const idVillage = searchParams.get("desa");
|
||||
|
||||
const cek = await prisma.discussion.count({
|
||||
where: {
|
||||
id,
|
||||
idVillage: String(idVillage)
|
||||
}
|
||||
})
|
||||
|
||||
if (cek == 0) {
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, data tidak ditemukan" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (kategori == "comment") {
|
||||
const data = await prisma.discussionComment.findMany({
|
||||
where: {
|
||||
idDiscussion: id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
comment: true,
|
||||
createdAt: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
img: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dataFix = data.map((v: any) => ({
|
||||
..._.omit(v, ["User",]),
|
||||
username: v.User.name,
|
||||
img: v.User.img
|
||||
}))
|
||||
|
||||
} else if (kategori == "member") {
|
||||
const data = await prisma.discussionMember.findMany({
|
||||
where: {
|
||||
idDiscussion: id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
img: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dataFix = data.map((v: any) => ({
|
||||
..._.omit(v, ["User",]),
|
||||
name: v.User.name,
|
||||
img: v.User.img
|
||||
}))
|
||||
} else {
|
||||
const data = await prisma.discussion.findUnique({
|
||||
where: {
|
||||
id,
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
select: {
|
||||
isActive: true,
|
||||
id: true,
|
||||
title: true,
|
||||
idGroup: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
Group: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dataFix = {
|
||||
id: data?.id,
|
||||
isActive: data?.isActive,
|
||||
idGroup: data?.idGroup,
|
||||
group: data?.Group.name,
|
||||
title: data?.title,
|
||||
desc: data?.desc,
|
||||
status: data?.status == 1 ? "Open" : "Close",
|
||||
createdAt: data?.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: dataFix }, { status: 200 });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
92
src/app/api/ai/discussion-general/route.ts
Normal file
92
src/app/api/ai/discussion-general/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DISCUSSION GENERAL
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idGroup = searchParams.get("group");
|
||||
const idVillage = searchParams.get("desa");
|
||||
const search = searchParams.get('search');
|
||||
const page = searchParams.get('page');
|
||||
const status = searchParams.get('status');
|
||||
const active = searchParams.get('active');
|
||||
const get = searchParams.get('get')
|
||||
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: active == "false" ? false : true,
|
||||
status: status == "close" ? 2 : 1,
|
||||
idVillage: String(idVillage),
|
||||
title: {
|
||||
contains: (search == undefined || search == "null") ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
|
||||
if (idGroup != "null" && idGroup != undefined && idGroup != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idGroup: String(idGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const data = await prisma.discussion.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
orderBy: [
|
||||
{
|
||||
status: 'desc'
|
||||
},
|
||||
{
|
||||
createdAt: 'desc'
|
||||
}
|
||||
],
|
||||
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
DiscussionComment: {
|
||||
select: {
|
||||
id: true,
|
||||
}
|
||||
},
|
||||
Group: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fixData = data.map((v: any) => ({
|
||||
..._.omit(v, ["DiscussionComment", "status", "Group"]),
|
||||
totalKomentar: v.DiscussionComment.length,
|
||||
status: v.status == 1 ? "Open" : "Close",
|
||||
group: v.Group.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: fixData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
92
src/app/api/ai/discussion/[id]/route.ts
Normal file
92
src/app/api/ai/discussion/[id]/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET ONE DISCUSSION BY ID
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params
|
||||
|
||||
const cek = await prisma.divisionDisscussion.count({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (cek == 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Gagal mendapatkan diskusi, data tidak ditemukan" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await prisma.divisionDisscussion.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
isActive: true,
|
||||
id: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idDivision: true,
|
||||
Division: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
User: { select: { name: true, img: true } },
|
||||
DivisionDisscussionComment: {
|
||||
select: {
|
||||
id: true,
|
||||
comment: true,
|
||||
createdAt: true,
|
||||
User: { select: { name: true, img: true } }
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Diskusi tidak ditemukan" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// ambil nama creator
|
||||
const createdBy = data.User.name;
|
||||
const status = data.status == 1 ? "Open" : "Close"
|
||||
const division = data.Division.name
|
||||
|
||||
// mapping komentar → hilangkan nested User
|
||||
const komentar = data.DivisionDisscussionComment.map((comment: any) => ({
|
||||
id: comment.id,
|
||||
comment: comment.comment,
|
||||
createdAt: comment.createdAt,
|
||||
username: comment.User.name,
|
||||
userimg: comment.User.img,
|
||||
}));
|
||||
|
||||
// bentuk hasil akhir sesuai request
|
||||
const result = {
|
||||
id: data.id,
|
||||
idDivision: data.idDivision,
|
||||
division,
|
||||
isActive: data.isActive,
|
||||
desc: data.desc,
|
||||
status,
|
||||
createdAt: data.createdAt,
|
||||
createdBy,
|
||||
komentar,
|
||||
};
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, message: "Berhasil mendapatkan diskusi", data: result },
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
95
src/app/api/ai/discussion/route.ts
Normal file
95
src/app/api/ai/discussion/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DISCUSSION DIVISION ACTIVE = TRUE
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idDivision = searchParams.get("division");
|
||||
const search = searchParams.get('search');
|
||||
const page = searchParams.get('page');
|
||||
const status = searchParams.get('status');
|
||||
const isActive = searchParams.get('active');
|
||||
const villageId = searchParams.get('desa');
|
||||
const get = searchParams.get('get');
|
||||
|
||||
let getFix = 0;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: isActive == "false" ? false : true,
|
||||
status: status == "close" ? 2 : 1,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
},
|
||||
desc: {
|
||||
contains: (search == undefined || search == "null") ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if (idDivision != "null" && idDivision != null && idDivision != undefined) {
|
||||
kondisi = {
|
||||
isActive: isActive == "false" ? false : true,
|
||||
status: status == "close" ? 2 : 1,
|
||||
idDivision: idDivision,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
},
|
||||
desc: {
|
||||
contains: (search == undefined || search == "null") ? "" : search,
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.divisionDisscussion.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idDivision: true,
|
||||
Division: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
DivisionDisscussionComment: {
|
||||
select: {
|
||||
id: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fixData = data.map((v: any) => ({
|
||||
..._.omit(v, ["DivisionDisscussionComment", "status", "Division"]),
|
||||
totalKomentar: v.DivisionDisscussionComment.length,
|
||||
status: v.status == 1 ? "Open" : "Close",
|
||||
division: v.Division.name
|
||||
}))
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: fixData, }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
63
src/app/api/ai/division/[id]/route.ts
Normal file
63
src/app/api/ai/division/[id]/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ONE DATA DIVISI :: UNTUK TAMPIL DATA DI HALAMAN EDIT DAN INFO
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idVillage = searchParams.get("desa");
|
||||
|
||||
const data = await prisma.division.findUnique({
|
||||
where: {
|
||||
id: String(id),
|
||||
idVillage: String(idVillage)
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, data tidak ditemukan", }, { status: 404 });
|
||||
}
|
||||
|
||||
const member = await prisma.divisionMember.findMany({
|
||||
where: {
|
||||
idDivision: String(id),
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isAdmin: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
img: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
isAdmin: 'desc',
|
||||
}
|
||||
})
|
||||
|
||||
const fixMember = member.map((v: any) => ({
|
||||
..._.omit(v, ["User"]),
|
||||
name: v.User.name,
|
||||
img: v.User.img
|
||||
}))
|
||||
|
||||
const dataFix = {
|
||||
...data,
|
||||
member: fixMember
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: dataFix, }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
269
src/app/api/ai/division/report/route.ts
Normal file
269
src/app/api/ai/division/report/route.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const idVillage = searchParams.get("desa")
|
||||
const idGroup = searchParams.get("group")
|
||||
const division = searchParams.get("division")
|
||||
const date = searchParams.get("date-start")
|
||||
const dateAkhir = searchParams.get("date-end")
|
||||
const kat = searchParams.get("cat")
|
||||
|
||||
|
||||
// CHART PROGRESS
|
||||
if (kat == "dokumen") {
|
||||
// CHART DOKUMEN
|
||||
let kondisi: any = {
|
||||
isActive: true,
|
||||
category: 'FILE',
|
||||
Division: {
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
createdAt: {
|
||||
gte: new Date(String(date)),
|
||||
lte: new Date(String(dateAkhir))
|
||||
},
|
||||
}
|
||||
|
||||
if (idGroup != undefined && idGroup != null && idGroup != "") {
|
||||
kondisi = {
|
||||
isActive: true,
|
||||
category: 'FILE',
|
||||
Division: {
|
||||
idGroup: String(idGroup)
|
||||
},
|
||||
createdAt: {
|
||||
gte: new Date(String(date)),
|
||||
lte: new Date(String(dateAkhir))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (division != undefined && division != null && division != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idDivision: String(division)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const dataDokumen = await prisma.divisionDocumentFolderFile.findMany({
|
||||
where: kondisi,
|
||||
})
|
||||
|
||||
const groupData = _.map(_.groupBy(dataDokumen, "extension"), (v: any) => ({
|
||||
file: v[0].extension,
|
||||
jumlah: v.length,
|
||||
}))
|
||||
|
||||
const image = ['jpg', 'jpeg', 'png', 'heic']
|
||||
|
||||
let hasilImage = {
|
||||
name: 'Gambar',
|
||||
value: 0
|
||||
}
|
||||
|
||||
let hasilFile = {
|
||||
name: 'Dokumen',
|
||||
value: 0
|
||||
}
|
||||
|
||||
groupData.map((v: any) => {
|
||||
if (image.some((i: any) => i == v.file)) {
|
||||
hasilImage = {
|
||||
name: 'Gambar',
|
||||
value: hasilImage.value + v.jumlah
|
||||
}
|
||||
} else {
|
||||
hasilFile = {
|
||||
name: 'Dokumen',
|
||||
value: hasilFile.value + v.jumlah
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const hasilDokumen = { gambar: hasilImage.value, dokumen: hasilFile.value }
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: hasilDokumen }, { status: 200 });
|
||||
} else if (kat == "event") {
|
||||
// CHART EVENT
|
||||
let kondisiSelesai: any = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
DivisionCalendarReminder: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gte: new Date(String(date)),
|
||||
lte: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let kondisiComingSoon: any = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
DivisionCalendarReminder: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gt: new Date(),
|
||||
lte: new Date(String(dateAkhir))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (idGroup != undefined && idGroup != null && idGroup != "") {
|
||||
kondisiSelesai = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idGroup: String(idGroup)
|
||||
},
|
||||
DivisionCalendarReminder: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gte: new Date(String(date)),
|
||||
lte: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kondisiComingSoon = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idGroup: String(idGroup)
|
||||
},
|
||||
DivisionCalendarReminder: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gt: new Date(),
|
||||
lte: new Date(String(dateAkhir))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (division != undefined && division != null && division != "") {
|
||||
kondisiSelesai = {
|
||||
...kondisiSelesai,
|
||||
idDivision: String(division)
|
||||
}
|
||||
|
||||
kondisiComingSoon = {
|
||||
...kondisiComingSoon,
|
||||
idDivision: String(division)
|
||||
}
|
||||
}
|
||||
|
||||
const eventSelesai = await prisma.divisionCalendar.count({
|
||||
where: kondisiSelesai
|
||||
})
|
||||
|
||||
const eventComingSoon = await prisma.divisionCalendar.count({
|
||||
where: kondisiComingSoon
|
||||
})
|
||||
|
||||
const hasilEvent = {
|
||||
selesai: eventSelesai,
|
||||
akan_datang: eventComingSoon
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: hasilEvent }, { status: 200 });
|
||||
|
||||
} else {
|
||||
let kondisiProgress: any = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
DivisionProjectTask: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gte: new Date(String(date))
|
||||
},
|
||||
dateEnd: {
|
||||
lte: new Date(String(dateAkhir))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (idGroup != undefined && idGroup != null && idGroup != "") {
|
||||
kondisiProgress = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idGroup: String(idGroup)
|
||||
},
|
||||
DivisionProjectTask: {
|
||||
some: {
|
||||
dateStart: {
|
||||
gte: new Date(String(date))
|
||||
},
|
||||
dateEnd: {
|
||||
lte: new Date(String(dateAkhir))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (division != undefined && division != null && division != "") {
|
||||
kondisiProgress = {
|
||||
...kondisiProgress,
|
||||
idDivision: String(division)
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.divisionProject.groupBy({
|
||||
where: kondisiProgress,
|
||||
by: ["status"],
|
||||
_count: true
|
||||
})
|
||||
|
||||
const dataStatus = [{ name: 'Segera', status: 0 }, { name: 'Dikerjakan', status: 1 }, { name: 'Selesai', status: 2 }, { name: 'Dibatalkan', status: 3 }]
|
||||
const hasilProgres: any[] = []
|
||||
let input
|
||||
for (let index = 0; index < dataStatus.length; index++) {
|
||||
const cek = data.some((i: any) => i.status == dataStatus[index].status)
|
||||
if (cek) {
|
||||
const find = ((Number(data.find((i: any) => i.status == dataStatus[index].status)?._count) * 100) / data.reduce((n, { _count }) => n + _count, 0)).toFixed(2)
|
||||
const fix = find != "100.00" ? find.substr(-2, 2) == "00" ? find.substr(0, 2) : find : "100"
|
||||
input = {
|
||||
name: dataStatus[index].name,
|
||||
value: fix
|
||||
}
|
||||
} else {
|
||||
input = {
|
||||
name: dataStatus[index].name,
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
hasilProgres.push(input)
|
||||
}
|
||||
|
||||
const dataFixProgress = hasilProgres.reduce((acc: any, curr: any) => {
|
||||
acc[curr.name] = curr.value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: dataFixProgress }, { status: 200 });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan data, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
84
src/app/api/ai/division/route.ts
Normal file
84
src/app/api/ai/division/route.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DATA DIVISI == LIST DATA DIVISI
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idVillage = searchParams.get("desa");
|
||||
const idGroup = searchParams.get("group");
|
||||
const name = searchParams.get('search');
|
||||
const page = searchParams.get('page');
|
||||
const active = searchParams.get("active");
|
||||
const get = searchParams.get('get')
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: active == 'false' ? false : true,
|
||||
idVillage: String(idVillage),
|
||||
name: {
|
||||
contains: (name == undefined || name == "null") ? "" : name,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
if (idGroup != "null" && idGroup != undefined && idGroup != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idGroup: String(idGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const data = await prisma.division.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
desc: true,
|
||||
idGroup: true,
|
||||
Group: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
DivisionMember: {
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
idUser: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
const allData = data.map((v: any) => ({
|
||||
..._.omit(v, ["DivisionMember", "Group"]),
|
||||
group: v.Group.name,
|
||||
jumlahMember: v.DivisionMember.length,
|
||||
}))
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: allData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
145
src/app/api/ai/document/route.ts
Normal file
145
src/app/api/ai/document/route.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DOCUMENT
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idDivision = searchParams.get("division");
|
||||
const villageId = searchParams.get("desa");
|
||||
const path = searchParams.get("path");
|
||||
const active = searchParams.get("active");
|
||||
const search = searchParams.get("search");
|
||||
const page = searchParams.get("page");
|
||||
const get = searchParams.get("get");
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
},
|
||||
isActive: active == 'false' ? false : true,
|
||||
path: (path == "undefined" || path == "null" || path == "" || path == null || path == undefined) ? "home" : path,
|
||||
name: {
|
||||
contains: (search == undefined || search == "null") ? "" : search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
if (idDivision != "null" && idDivision != undefined && idDivision != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idDivision: String(idDivision)
|
||||
}
|
||||
}
|
||||
|
||||
let formatDataShare: any[] = [];
|
||||
|
||||
if (path == "home" || path == "null" || path == "undefined" || path == null || path == undefined || path == "") {
|
||||
const dataShare = await prisma.divisionDocumentShare.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idDivision: String(idDivision),
|
||||
DivisionDocumentFolderFile: {
|
||||
isActive: true
|
||||
}
|
||||
},
|
||||
select: {
|
||||
DivisionDocumentFolderFile: {
|
||||
select: {
|
||||
idStorage: true,
|
||||
id: true,
|
||||
category: true,
|
||||
name: true,
|
||||
extension: true,
|
||||
path: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
DivisionDocumentFolderFile: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
formatDataShare = dataShare.map((v: any) => ({
|
||||
..._.omit(v, ["DivisionDocumentFolderFile"]),
|
||||
idStorage: v.DivisionDocumentFolderFile.idStorage,
|
||||
id: v.DivisionDocumentFolderFile.id,
|
||||
category: v.DivisionDocumentFolderFile.category,
|
||||
name: v.DivisionDocumentFolderFile.name,
|
||||
extension: v.DivisionDocumentFolderFile.extension,
|
||||
path: v.DivisionDocumentFolderFile.path,
|
||||
createdBy: v.DivisionDocumentFolderFile.User.name,
|
||||
createdAt: v.DivisionDocumentFolderFile.createdAt,
|
||||
updatedAt: v.DivisionDocumentFolderFile.updatedAt,
|
||||
share: true
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const data = await prisma.divisionDocumentFolderFile.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
category: true,
|
||||
name: true,
|
||||
extension: true,
|
||||
idStorage: true,
|
||||
path: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
const allData = data.map((v: any) => ({
|
||||
..._.omit(v, ["User", "createdAt", "updatedAt"]),
|
||||
createdBy: v.User.name,
|
||||
createdAt: v.createdAt,
|
||||
updatedAt: v.updatedAt,
|
||||
share: false
|
||||
}))
|
||||
|
||||
if (formatDataShare.length > 0) {
|
||||
allData.push(...formatDataShare)
|
||||
}
|
||||
|
||||
const formatData = _.orderBy(allData, ['category', 'createdAt'], ['desc', 'desc']);
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan item", data: formatData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan item, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/ai/group/route.ts
Normal file
45
src/app/api/ai/group/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const villageId = searchParams.get("desa");
|
||||
const isActive = searchParams.get("active");
|
||||
const search = searchParams.get('search');
|
||||
const page = searchParams.get('page')
|
||||
const get = searchParams.get('get')
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
const data = await prisma.group.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: {
|
||||
isActive: isActive == 'false' ? false : true,
|
||||
idVillage: String(villageId),
|
||||
name: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan grup", data, }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan grup, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
79
src/app/api/ai/position/route.ts
Normal file
79
src/app/api/ai/position/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL POSITION
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idVillage = searchParams.get("desa");
|
||||
const idGroup = searchParams.get("group");
|
||||
const active = searchParams.get('active');
|
||||
const search = searchParams.get('search')
|
||||
const page = searchParams.get('page')
|
||||
const get = searchParams.get('get')
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: active == 'false' ? false : true,
|
||||
Group: {
|
||||
idVillage: String(idVillage)
|
||||
},
|
||||
name: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
if (idGroup != "null" && idGroup != undefined && idGroup != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idGroup: String(idGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const positions = await prisma.position.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
idGroup: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
Group: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
const allData = positions.map((v: any) => ({
|
||||
..._.omit(v, ["Group"]),
|
||||
group: v.Group.name
|
||||
}))
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan jabatan", data: allData }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan jabatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
172
src/app/api/ai/project/[id]/route.ts
Normal file
172
src/app/api/ai/project/[id]/route.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET DETAIL PROJECT / GET ONE PROJECT
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
let allData
|
||||
const { id } = context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kategori = searchParams.get("cat");
|
||||
|
||||
const data = await prisma.project.findUnique({
|
||||
where: {
|
||||
id: String(id),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idVillage: true,
|
||||
idGroup: true,
|
||||
title: true,
|
||||
status: true,
|
||||
desc: true,
|
||||
reason: true,
|
||||
report: true,
|
||||
isActive: true,
|
||||
Group: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, data tidak ditemukan", }, { status: 404 });
|
||||
}
|
||||
|
||||
|
||||
if (kategori == "data") {
|
||||
const dataProgress = await prisma.projectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
const semua = dataProgress.length
|
||||
const selesai = _.filter(dataProgress, { status: 1 }).length
|
||||
const progress = Math.ceil((selesai / semua) * 100)
|
||||
|
||||
allData = {
|
||||
id: data.id,
|
||||
idVillage: data.idVillage,
|
||||
idGroup: data.idGroup,
|
||||
group: data.Group.name,
|
||||
title: data.title,
|
||||
status: data.status == 3 ? "batal" : data.status == 2 ? "selesai" : data.status == 1 ? "dikerjakan" : "segera",
|
||||
desc: data.desc,
|
||||
reason: data.reason,
|
||||
report: data.report,
|
||||
isActive: data.isActive,
|
||||
progress: (_.isNaN(progress)) ? 0 : progress,
|
||||
}
|
||||
|
||||
} else if (kategori == "task") {
|
||||
const dataProgress = await prisma.projectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
dateStart: true,
|
||||
dateEnd: true,
|
||||
createdAt: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
}
|
||||
})
|
||||
|
||||
const formatData = dataProgress.map((v: any) => ({
|
||||
..._.omit(v, ["dateStart", "dateEnd", "createdAt", "status"]),
|
||||
status: v.status == 1 ? "selesai" : "belum selesai",
|
||||
dateStart: moment(v.dateStart).format("DD-MM-YYYY"),
|
||||
dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"),
|
||||
createdAt: moment(v.createdAt).format("DD-MM-YYYY HH:mm"),
|
||||
}))
|
||||
const dataFix = _.orderBy(formatData, 'createdAt', 'asc')
|
||||
allData = dataFix
|
||||
|
||||
} else if (kategori == "file") {
|
||||
const dataFile = await prisma.projectFile.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
extension: true,
|
||||
idStorage: true
|
||||
}
|
||||
})
|
||||
|
||||
allData = dataFile
|
||||
|
||||
} else if (kategori == "member") {
|
||||
const dataMember = await prisma.projectMember.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
img: true,
|
||||
Position: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const fix = dataMember.map((v: any) => ({
|
||||
..._.omit(v, ["User"]),
|
||||
name: v.User.name,
|
||||
email: v.User.email,
|
||||
img: v.User.img,
|
||||
position: v.User.Position.name
|
||||
}))
|
||||
|
||||
allData = fix
|
||||
} else if (kategori == "link") {
|
||||
const dataLink = await prisma.projectLink.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
}
|
||||
})
|
||||
allData = dataLink
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan kegiatan", data: allData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
107
src/app/api/ai/project/route.ts
Normal file
107
src/app/api/ai/project/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _, { ceil } from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DATA PROJECT
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const idVillage = searchParams.get('desa');
|
||||
const name = searchParams.get('search');
|
||||
const status = searchParams.get('status');
|
||||
const idGroup = searchParams.get("group");
|
||||
const page = searchParams.get('page');
|
||||
const get = searchParams.get('get');
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
|
||||
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: true,
|
||||
idVillage: String(idVillage),
|
||||
title: {
|
||||
contains: (name == undefined || name == "null") ? "" : name,
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
|
||||
if (status != "null" && status != undefined && status != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
status: status == "segera" ? 0 : status == "dikerjakan" ? 1 : status == "selesai" ? 2 : status == "batal" ? 3 : 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (idGroup != "null" && idGroup != undefined && idGroup != "") {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idGroup: String(idGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const data = await prisma.project.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
idGroup: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
ProjectMember: {
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
idUser: true
|
||||
}
|
||||
},
|
||||
ProjectTask: {
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
status: true
|
||||
}
|
||||
},
|
||||
Group: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
const omitData = data.map((v: any) => ({
|
||||
..._.omit(v, ["ProjectMember", "ProjectTask", "status", "Group"]),
|
||||
group: v.Group.name,
|
||||
status: v.status == 1 ? "dikerjakan" : v.status == 2 ? "selesai" : v.status == 3 ? "batal" : "segera",
|
||||
progress: ceil((v.ProjectTask.filter((i: any) => i.status == 1).length * 100) / v.ProjectTask.length),
|
||||
member: v.ProjectMember.length
|
||||
}))
|
||||
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan kegiatan", data: omitData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
180
src/app/api/ai/task/[id]/route.ts
Normal file
180
src/app/api/ai/task/[id]/route.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET DETAIL TASK DIVISI / GET ONE
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
let allData
|
||||
const { id } = context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kategori = searchParams.get("cat");
|
||||
|
||||
const data = await prisma.divisionProject.findUnique({
|
||||
where: {
|
||||
id: String(id),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idDivision: true,
|
||||
title: true,
|
||||
status: true,
|
||||
desc: true,
|
||||
reason: true,
|
||||
report: true,
|
||||
isActive: true,
|
||||
Division: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, data tidak ditemukan", }, { status: 404 });
|
||||
}
|
||||
|
||||
if (kategori == "data") {
|
||||
const dataProgress = await prisma.divisionProjectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
const semua = dataProgress.length
|
||||
const selesai = _.filter(dataProgress, { status: 1 }).length
|
||||
const progress = Math.ceil((selesai / semua) * 100)
|
||||
|
||||
|
||||
allData = {
|
||||
id: data.id,
|
||||
idDivision: data.idDivision,
|
||||
division: data.Division.name,
|
||||
title: data.title,
|
||||
status: data.status == 3 ? "batal" : data.status == 2 ? "selesai" : data.status == 1 ? "dikerjakan" : "segera",
|
||||
desc: data.desc,
|
||||
reason: data.reason,
|
||||
report: data.report,
|
||||
isActive: data.isActive,
|
||||
progress: progress,
|
||||
}
|
||||
} else if (kategori == "task") {
|
||||
const dataProgress = await prisma.divisionProjectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
status: true,
|
||||
dateStart: true,
|
||||
dateEnd: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
}
|
||||
})
|
||||
|
||||
const fix = dataProgress.map((v: any) => ({
|
||||
..._.omit(v, ["dateStart", "dateEnd", "status"]),
|
||||
status: v.status == 1 ? "selesai" : "belum selesai",
|
||||
dateStart: moment(v.dateStart).format("DD-MM-YYYY"),
|
||||
dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"),
|
||||
}))
|
||||
|
||||
allData = fix
|
||||
|
||||
} else if (kategori == "file") {
|
||||
const dataFile = await prisma.divisionProjectFile.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
ContainerFileDivision: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
extension: true,
|
||||
idStorage: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const fix = dataFile.map((v: any) => ({
|
||||
..._.omit(v, ["ContainerFileDivision"]),
|
||||
nameInStorage: v.ContainerFileDivision.id,
|
||||
name: v.ContainerFileDivision.name,
|
||||
extension: v.ContainerFileDivision.extension,
|
||||
idStorage: v.ContainerFileDivision.idStorage,
|
||||
}))
|
||||
|
||||
allData = fix
|
||||
|
||||
} else if (kategori == "member") {
|
||||
const dataMember = await prisma.divisionProjectMember.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
img: true,
|
||||
Position: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const fix = dataMember.map((v: any) => ({
|
||||
..._.omit(v, ["User"]),
|
||||
name: v.User.name,
|
||||
email: v.User.email,
|
||||
img: v.User.img,
|
||||
position: v.User.Position.name
|
||||
}))
|
||||
|
||||
allData = fix
|
||||
} else if (kategori == "link") {
|
||||
const dataLink = await prisma.divisionProjectLink.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idProject: String(id)
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
}
|
||||
})
|
||||
|
||||
allData = dataLink
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan tugas divisi", data: allData }, { status: 200 });
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan tugas divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
104
src/app/api/ai/task/route.ts
Normal file
104
src/app/api/ai/task/route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _, { ceil } from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
// GET ALL DATA TUGAS DIVISI
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const villageId = searchParams.get('desa');
|
||||
const division = searchParams.get('division');
|
||||
const search = searchParams.get('search');
|
||||
const status = searchParams.get('status');
|
||||
const page = searchParams.get('page');
|
||||
const get = searchParams.get('get');
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: true,
|
||||
Division: {
|
||||
idVillage: String(villageId)
|
||||
},
|
||||
title: {
|
||||
contains: (search == undefined || search == "null") ? "" : search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
|
||||
if (status != "null" && status != undefined && status != "" && status != null) {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
status: status == "segera" ? 0 : status == "dikerjakan" ? 1 : status == "selesai" ? 2 : status == "batal" ? 3 : 0
|
||||
}
|
||||
}
|
||||
|
||||
if (division != "null" && division != undefined && division != "" && division != null) {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idDivision: String(division)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const data = await prisma.divisionProject.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
idDivision: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
status: true,
|
||||
DivisionProjectTask: {
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
status: true
|
||||
}
|
||||
},
|
||||
DivisionProjectMember: {
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
idUser: true
|
||||
}
|
||||
},
|
||||
Division: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
}
|
||||
});
|
||||
|
||||
const formatData = data.map((v: any) => ({
|
||||
..._.omit(v, ["DivisionProjectTask", "DivisionProjectMember", "status", "Division"]),
|
||||
division: v.Division.name,
|
||||
status: v.status == 1 ? "dikerjakan" : v.status == 2 ? "selesai" : v.status == 3 ? "batal" : "segera",
|
||||
progress: ceil((v.DivisionProjectTask.filter((i: any) => i.status == 1).length * 100) / v.DivisionProjectTask.length),
|
||||
member: v.DivisionProjectMember.length,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: formatData }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
75
src/app/api/ai/user/[id]/route.ts
Normal file
75
src/app/api/ai/user/[id]/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET ONE MEMBER / USER
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
|
||||
const users = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
nik: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
email: true,
|
||||
gender: true,
|
||||
img: true,
|
||||
idGroup: true,
|
||||
isActive: true,
|
||||
idPosition: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
UserRole: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true
|
||||
}
|
||||
},
|
||||
Position: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { ...userData } = users;
|
||||
const group = users?.Group.name
|
||||
const position = users?.Position?.name
|
||||
const idUserRole = users?.UserRole.id
|
||||
const phone = '+62' + users?.phone
|
||||
const role = users?.UserRole.name
|
||||
const gender = users?.gender == "F" ? "Perempuan" : "Laki-Laki"
|
||||
|
||||
const result = { ...userData, gender, group, position, idUserRole, phone, role };
|
||||
|
||||
const omitData = _.omit(result, ["Group", "Position", "UserRole"]);
|
||||
|
||||
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Berhasil mendapatkan anggota",
|
||||
data: omitData,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan anggota, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
88
src/app/api/ai/user/route.ts
Normal file
88
src/app/api/ai/user/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET ALL MEMBER / USER
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const search = searchParams.get('search')
|
||||
const idVillage = searchParams.get("desa");
|
||||
const idGroup = searchParams.get("group");
|
||||
const active = searchParams.get("active");
|
||||
const page = searchParams.get('page');
|
||||
const get = searchParams.get('get');
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
let kondisi: any = {
|
||||
isActive: active == 'false' ? false : true,
|
||||
idVillage: String(idVillage),
|
||||
name: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive",
|
||||
},
|
||||
NOT: {
|
||||
idUserRole: 'developer'
|
||||
}
|
||||
}
|
||||
|
||||
if (idGroup != "null" && idGroup != undefined && idGroup != "" && idGroup != null) {
|
||||
kondisi = {
|
||||
...kondisi,
|
||||
idGroup: String(idGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: kondisi,
|
||||
select: {
|
||||
id: true,
|
||||
idUserRole: true,
|
||||
isActive: true,
|
||||
nik: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
Position: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
const allData = users.map((v: any) => ({
|
||||
..._.omit(v, ["phone", "gender", "Group", "Position"]),
|
||||
gender: v.gender == "F" ? "Perempuan" : "Laki-Laki",
|
||||
phone: "+" + v.phone,
|
||||
group: v.Group.name,
|
||||
position: v?.Position?.name
|
||||
}))
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil member", data: allData }, { status: 200 });
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan anggota, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
50
src/app/api/ai/village/route.ts
Normal file
50
src/app/api/ai/village/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import _ from "lodash";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const isActive = searchParams.get("active");
|
||||
const search = searchParams.get('search');
|
||||
const page = searchParams.get('page')
|
||||
const get = searchParams.get('get')
|
||||
|
||||
let getFix = 10;
|
||||
if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) {
|
||||
getFix = 10;
|
||||
} else {
|
||||
getFix = Number(get);
|
||||
}
|
||||
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix;
|
||||
|
||||
const data = await prisma.village.findMany({
|
||||
skip: dataSkip,
|
||||
take: getFix,
|
||||
where: {
|
||||
isActive: isActive == 'false' ? false : true,
|
||||
name: {
|
||||
contains: (search == undefined || search == null) ? "" : search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan desa", data, }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan desa, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -68,18 +68,7 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
status: true,
|
||||
dateStart: true,
|
||||
dateEnd: true,
|
||||
createdAt: true,
|
||||
ProjectTaskFile: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
ProjectFile: {
|
||||
select: {
|
||||
name: true,
|
||||
extension: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt: true
|
||||
},
|
||||
orderBy: {
|
||||
dateStart: 'asc'
|
||||
@@ -87,15 +76,12 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
})
|
||||
|
||||
const formatData = dataProgress.map((v: any) => ({
|
||||
..._.omit(v, ["dateStart", "dateEnd", "createdAt", "ProjectTaskFile"]),
|
||||
dateStart: moment(v.dateStart).format("DD MMM YYYY"),
|
||||
dateEnd: moment(v.dateEnd).format("DD MMM YYYY"),
|
||||
..._.omit(v, ["dateStart", "dateEnd", "createdAt"]),
|
||||
dateStart: moment(v.dateStart).format("DD-MM-YYYY"),
|
||||
dateEnd: moment(v.dateEnd).format("DD-MM-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") {
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
import { funSendWebPush, prisma } from "@/module/_global";
|
||||
import { funGetUserById } from "@/module/auth";
|
||||
import { createLogUserMobile } from "@/module/user";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
import { sendFCMNotificationMany } from "../../../../../../../../xsendMany";
|
||||
|
||||
const APPROVER_ROLES = ['supadmin', 'developer'];
|
||||
|
||||
async function getApproverStatus(userId: string): Promise<boolean> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
isApprover: true,
|
||||
UserRole: { select: { id: true } }
|
||||
}
|
||||
});
|
||||
if (!user) return false;
|
||||
return user.isApprover || APPROVER_ROLES.includes(user.UserRole.id);
|
||||
}
|
||||
|
||||
async function recalculateProjectStatus(idProject: string) {
|
||||
const tasks = await prisma.projectTask.findMany({
|
||||
where: { isActive: true, idProject },
|
||||
select: { status: true }
|
||||
});
|
||||
|
||||
const semua = tasks.length;
|
||||
const selesai = tasks.filter((t) => t.status === 1).length;
|
||||
const prosess = semua === 0 ? 0 : Math.ceil((selesai / semua) * 100);
|
||||
|
||||
let statusProject = 1;
|
||||
if (prosess === 100) statusProject = 2;
|
||||
else if (prosess === 0) statusProject = 0;
|
||||
|
||||
await prisma.project.update({
|
||||
where: { id: idProject },
|
||||
data: { status: statusProject }
|
||||
});
|
||||
}
|
||||
|
||||
type NotifTarget = {
|
||||
idUserTo: string;
|
||||
tokens: string[];
|
||||
subscription: string | undefined;
|
||||
}
|
||||
|
||||
async function sendNotification({
|
||||
targets,
|
||||
idUserFrom,
|
||||
idContent,
|
||||
title,
|
||||
desc,
|
||||
}: {
|
||||
targets: NotifTarget[];
|
||||
idUserFrom: string;
|
||||
idContent: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
}) {
|
||||
const filtered = targets.filter((t) => t.idUserTo !== idUserFrom);
|
||||
const unique = _.uniqBy(filtered, 'idUserTo');
|
||||
|
||||
if (unique.length === 0) return;
|
||||
|
||||
// In-app notification
|
||||
await prisma.notifications.createMany({
|
||||
data: unique.map((t) => ({
|
||||
idUserTo: t.idUserTo,
|
||||
idUserFrom,
|
||||
category: 'project',
|
||||
idContent,
|
||||
title,
|
||||
desc,
|
||||
}))
|
||||
});
|
||||
|
||||
// FCM push notification
|
||||
const tokens = [...new Set(unique.flatMap((t) => t.tokens))].filter(Boolean);
|
||||
if (tokens.length > 0) {
|
||||
await sendFCMNotificationMany({
|
||||
token: tokens,
|
||||
title,
|
||||
body: desc,
|
||||
data: { id: idContent, category: 'project', content: idContent }
|
||||
});
|
||||
}
|
||||
|
||||
// Web push notification
|
||||
const subs = unique
|
||||
.filter((t): t is typeof t & { subscription: string } => Boolean(t.subscription))
|
||||
.map((t) => ({ idUser: t.idUserTo, subscription: t.subscription }));
|
||||
if (subs.length > 0) {
|
||||
await funSendWebPush({ sub: subs, message: { title, body: desc } });
|
||||
}
|
||||
}
|
||||
|
||||
async function getApproversInVillage(idVillage: string, idGroup: string): Promise<NotifTarget[]> {
|
||||
const approvers = await prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idVillage,
|
||||
OR: [
|
||||
{ isApprover: true, idGroup },
|
||||
{ UserRole: { id: 'supadmin' } }
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
TokenDeviceUser: { select: { token: true } },
|
||||
Subscribe: { select: { subscription: true } }
|
||||
}
|
||||
});
|
||||
|
||||
return approvers.map((u) => ({
|
||||
idUserTo: u.id,
|
||||
tokens: u.TokenDeviceUser.map((t) => t.token),
|
||||
subscription: u.Subscribe?.subscription ?? undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getUserNotifTarget(userId: string): Promise<NotifTarget | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
TokenDeviceUser: { select: { token: true } },
|
||||
Subscribe: { select: { subscription: true } }
|
||||
}
|
||||
});
|
||||
if (!user) return null;
|
||||
return {
|
||||
idUserTo: user.id,
|
||||
tokens: user.TokenDeviceUser.map((t) => t.token),
|
||||
subscription: user.Subscribe?.subscription ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// GET — Riwayat approval task
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const user = searchParams.get("user");
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.projectTask.count({ where: { id, isActive: true } });
|
||||
if (task === 0) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const data = await prisma.projectTaskApproval.findMany({
|
||||
where: { idTask: id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
Submitter: { select: { name: true } },
|
||||
Approver: { select: { name: true } },
|
||||
}
|
||||
});
|
||||
|
||||
const formatted = data.map((v) => ({
|
||||
id: v.id,
|
||||
status: v.status,
|
||||
note: v.note,
|
||||
createdAt: moment(v.createdAt).format("DD MMM YYYY, HH:mm"),
|
||||
submitter: { name: v.Submitter.name },
|
||||
approver: v.Approver ? { name: v.Approver.name } : null,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ success: true, message: "Riwayat approval berhasil ditemukan", data: formatted }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan riwayat approval (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// POST — Ajukan selesai (user mengajukan task untuk persetujuan)
|
||||
export async function POST(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { user } = await request.json();
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.projectTask.findUnique({
|
||||
where: { id, isActive: true },
|
||||
select: {
|
||||
id: true, status: true, title: true,
|
||||
Project: { select: { id: true, idGroup: true } }
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (task.status !== 0) {
|
||||
return NextResponse.json({ success: false, message: "Hanya tugas berstatus 'Belum Selesai' yang bisa diajukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const pendingApproval = await prisma.projectTaskApproval.count({
|
||||
where: { idTask: id, status: 0 }
|
||||
});
|
||||
|
||||
if (pendingApproval > 0) {
|
||||
return NextResponse.json({ success: false, message: "Tugas sudah dalam proses menunggu persetujuan" }, { status: 200 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.projectTaskApproval.create({
|
||||
data: { idTask: id, idUser: userMobile.id, status: 0 }
|
||||
}),
|
||||
prisma.projectTask.update({
|
||||
where: { id },
|
||||
data: { status: 2 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateProjectStatus(task.Project.id);
|
||||
|
||||
// Notifikasi ke semua approver di desa dan group yang sama
|
||||
const approverTargets = await getApproversInVillage(String(userMobile.idVillage), task.Project.idGroup);
|
||||
await sendNotification({
|
||||
targets: approverTargets,
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.Project.id,
|
||||
title: 'Pengajuan Penyelesaian Tugas',
|
||||
desc: task.title,
|
||||
});
|
||||
|
||||
await createLogUserMobile({ act: 'CREATE', desc: 'User mengajukan task untuk persetujuan', table: 'projectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil diajukan untuk persetujuan" }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mengajukan tugas (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PUT — Setujui atau Tolak (approver action)
|
||||
export async function PUT(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { user, action, note } = await request.json();
|
||||
|
||||
if (!['approve', 'reject'].includes(action)) {
|
||||
return NextResponse.json({ success: false, message: "Action tidak valid, gunakan 'approve' atau 'reject'" }, { status: 200 });
|
||||
}
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const canApprove = await getApproverStatus(userMobile.id);
|
||||
if (!canApprove) {
|
||||
return NextResponse.json({ success: false, message: "Anda tidak memiliki izin untuk menyetujui atau menolak tugas" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.projectTask.findUnique({
|
||||
where: { id, isActive: true },
|
||||
select: { id: true, status: true, title: true, Project: { select: { id: true } } }
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (task.status !== 2) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak sedang menunggu persetujuan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const pendingApproval = await prisma.projectTaskApproval.findFirst({
|
||||
where: { idTask: id, status: 0 },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, idUser: true }
|
||||
});
|
||||
|
||||
if (!pendingApproval) {
|
||||
return NextResponse.json({ success: false, message: "Data persetujuan pending tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (action === 'approve') {
|
||||
await prisma.$transaction([
|
||||
prisma.projectTaskApproval.update({
|
||||
where: { id: pendingApproval.id },
|
||||
data: { status: 1, idApprover: userMobile.id }
|
||||
}),
|
||||
prisma.projectTask.update({
|
||||
where: { id },
|
||||
data: { status: 1 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateProjectStatus(task.Project.id);
|
||||
|
||||
// Notifikasi ke submitter
|
||||
const submitterTarget = await getUserNotifTarget(pendingApproval.idUser);
|
||||
if (submitterTarget) {
|
||||
await sendNotification({
|
||||
targets: [submitterTarget],
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.Project.id,
|
||||
title: 'Tugas Disetujui',
|
||||
desc: task.title,
|
||||
});
|
||||
}
|
||||
|
||||
await createLogUserMobile({ act: 'UPDATE', desc: 'Approver menyetujui task', table: 'projectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil disetujui" }, { status: 200 });
|
||||
}
|
||||
|
||||
// reject
|
||||
if (!note || String(note).trim() === '') {
|
||||
return NextResponse.json({ success: false, message: "Alasan penolakan wajib diisi" }, { status: 200 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.projectTaskApproval.update({
|
||||
where: { id: pendingApproval.id },
|
||||
data: { status: 2, idApprover: userMobile.id, note: String(note).trim() }
|
||||
}),
|
||||
prisma.projectTask.update({
|
||||
where: { id },
|
||||
data: { status: 0 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateProjectStatus(task.Project.id);
|
||||
|
||||
// Notifikasi ke submitter
|
||||
const submitterTarget = await getUserNotifTarget(pendingApproval.idUser);
|
||||
if (submitterTarget) {
|
||||
await sendNotification({
|
||||
targets: [submitterTarget],
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.Project.id,
|
||||
title: 'Tugas Ditolak',
|
||||
desc: task.title,
|
||||
});
|
||||
}
|
||||
|
||||
await createLogUserMobile({ act: 'UPDATE', desc: 'Approver menolak task', table: 'projectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil ditolak" }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal memproses persetujuan (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
where: {
|
||||
id: String(id),
|
||||
isActive: true
|
||||
},
|
||||
include: {
|
||||
Division: { select: { idGroup: true } }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,7 +33,7 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
}
|
||||
|
||||
if (kategori == "data") {
|
||||
allData = { ...data, idGroup: data.Division.idGroup }
|
||||
allData = data
|
||||
} else if (kategori == "progress") {
|
||||
const dataProgress = await prisma.divisionProjectTask.findMany({
|
||||
where: {
|
||||
@@ -77,21 +74,6 @@ 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'
|
||||
@@ -99,13 +81,9 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
})
|
||||
|
||||
const fix = dataProgress.map((v: any) => ({
|
||||
..._.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,
|
||||
})),
|
||||
..._.omit(v, ["dateStart", "dateEnd"]),
|
||||
dateStart: moment(v.dateStart).format("DD-MM-YYYY"),
|
||||
dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"),
|
||||
}))
|
||||
|
||||
allData = fix
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
import { funSendWebPush, prisma } from "@/module/_global";
|
||||
import { funGetUserById } from "@/module/auth";
|
||||
import { createLogUserMobile } from "@/module/user";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
import { sendFCMNotificationMany } from "../../../../../../../../xsendMany";
|
||||
|
||||
const APPROVER_ROLES = ['supadmin', 'developer'];
|
||||
|
||||
async function getApproverStatus(userId: string): Promise<boolean> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
isApprover: true,
|
||||
UserRole: { select: { id: true } }
|
||||
}
|
||||
});
|
||||
if (!user) return false;
|
||||
return user.isApprover || APPROVER_ROLES.includes(user.UserRole.id);
|
||||
}
|
||||
|
||||
async function recalculateTaskStatus(idProject: string) {
|
||||
const tasks = await prisma.divisionProjectTask.findMany({
|
||||
where: { isActive: true, idProject },
|
||||
select: { status: true }
|
||||
});
|
||||
|
||||
const semua = tasks.length;
|
||||
const selesai = tasks.filter((t) => t.status === 1).length;
|
||||
const prosess = semua === 0 ? 0 : Math.ceil((selesai / semua) * 100);
|
||||
|
||||
let statusProject = 1;
|
||||
if (prosess === 100) statusProject = 2;
|
||||
else if (prosess === 0) statusProject = 0;
|
||||
|
||||
await prisma.divisionProject.update({
|
||||
where: { id: idProject },
|
||||
data: { status: statusProject }
|
||||
});
|
||||
}
|
||||
|
||||
type NotifTarget = {
|
||||
idUserTo: string;
|
||||
tokens: string[];
|
||||
subscription: string | undefined;
|
||||
}
|
||||
|
||||
async function sendNotification({
|
||||
targets,
|
||||
idUserFrom,
|
||||
idContent,
|
||||
category,
|
||||
title,
|
||||
desc,
|
||||
}: {
|
||||
targets: NotifTarget[];
|
||||
idUserFrom: string;
|
||||
idContent: string;
|
||||
category: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
}) {
|
||||
const filtered = targets.filter((t) => t.idUserTo !== idUserFrom);
|
||||
const unique = _.uniqBy(filtered, 'idUserTo');
|
||||
|
||||
if (unique.length === 0) return;
|
||||
|
||||
await prisma.notifications.createMany({
|
||||
data: unique.map((t) => ({
|
||||
idUserTo: t.idUserTo,
|
||||
idUserFrom,
|
||||
category,
|
||||
idContent,
|
||||
title,
|
||||
desc,
|
||||
}))
|
||||
});
|
||||
|
||||
const tokens = [...new Set(unique.flatMap((t) => t.tokens))].filter(Boolean);
|
||||
if (tokens.length > 0) {
|
||||
await sendFCMNotificationMany({
|
||||
token: tokens,
|
||||
title,
|
||||
body: desc,
|
||||
data: { id: idContent, category, content: idContent }
|
||||
});
|
||||
}
|
||||
|
||||
const subs = unique
|
||||
.filter((t): t is typeof t & { subscription: string } => Boolean(t.subscription))
|
||||
.map((t) => ({ idUser: t.idUserTo, subscription: t.subscription }));
|
||||
if (subs.length > 0) {
|
||||
await funSendWebPush({ sub: subs, message: { title, body: desc } });
|
||||
}
|
||||
}
|
||||
|
||||
async function getApproversForDivision(idVillage: string, idDivision: string): Promise<NotifTarget[]> {
|
||||
const division = await prisma.division.findUnique({
|
||||
where: { id: idDivision },
|
||||
select: { idGroup: true }
|
||||
});
|
||||
const idGroup = division?.idGroup;
|
||||
|
||||
const [globalApprovers, divisionAdmins] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
idVillage,
|
||||
OR: [
|
||||
{ isApprover: true, idGroup },
|
||||
{ UserRole: { id: 'supadmin' } }
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
TokenDeviceUser: { select: { token: true } },
|
||||
Subscribe: { select: { subscription: true } }
|
||||
}
|
||||
}),
|
||||
prisma.divisionMember.findMany({
|
||||
where: { idDivision, isAdmin: true, isActive: true },
|
||||
select: {
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
TokenDeviceUser: { select: { token: true } },
|
||||
Subscribe: { select: { subscription: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const fromGlobal = globalApprovers.map((u) => ({
|
||||
idUserTo: u.id,
|
||||
tokens: u.TokenDeviceUser.map((t) => t.token),
|
||||
subscription: u.Subscribe?.subscription ?? undefined,
|
||||
}));
|
||||
|
||||
const fromAdmin = divisionAdmins.map((m) => ({
|
||||
idUserTo: m.User.id,
|
||||
tokens: m.User.TokenDeviceUser.map((t) => t.token),
|
||||
subscription: m.User.Subscribe?.subscription ?? undefined,
|
||||
}));
|
||||
|
||||
return _.uniqBy([...fromGlobal, ...fromAdmin], 'idUserTo');
|
||||
}
|
||||
|
||||
async function getUserNotifTarget(userId: string): Promise<NotifTarget | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
TokenDeviceUser: { select: { token: true } },
|
||||
Subscribe: { select: { subscription: true } }
|
||||
}
|
||||
});
|
||||
if (!user) return null;
|
||||
return {
|
||||
idUserTo: user.id,
|
||||
tokens: user.TokenDeviceUser.map((t) => t.token),
|
||||
subscription: user.Subscribe?.subscription ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// GET — Riwayat approval task divisi
|
||||
export async function GET(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const user = searchParams.get("user");
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.divisionProjectTask.count({ where: { id, isActive: true } });
|
||||
if (task === 0) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const data = await prisma.divisionProjectTaskApproval.findMany({
|
||||
where: { idTask: id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
Submitter: { select: { name: true } },
|
||||
Approver: { select: { name: true } },
|
||||
}
|
||||
});
|
||||
|
||||
const formatted = data.map((v) => ({
|
||||
id: v.id,
|
||||
status: v.status,
|
||||
note: v.note,
|
||||
createdAt: moment(v.createdAt).format("DD MMM YYYY, HH:mm"),
|
||||
submitter: { name: v.Submitter.name },
|
||||
approver: v.Approver ? { name: v.Approver.name } : null,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ success: true, message: "Riwayat approval berhasil ditemukan", data: formatted }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan riwayat approval (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// POST — Ajukan selesai
|
||||
export async function POST(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { user } = await request.json();
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.divisionProjectTask.findUnique({
|
||||
where: { id, isActive: true },
|
||||
select: { id: true, status: true, idProject: true, idDivision: true, title: true }
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (task.status !== 0) {
|
||||
return NextResponse.json({ success: false, message: "Hanya tugas berstatus 'Belum Selesai' yang bisa diajukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const pendingApproval = await prisma.divisionProjectTaskApproval.count({
|
||||
where: { idTask: id, status: 0 }
|
||||
});
|
||||
|
||||
if (pendingApproval > 0) {
|
||||
return NextResponse.json({ success: false, message: "Tugas sudah dalam proses menunggu persetujuan" }, { status: 200 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.divisionProjectTaskApproval.create({
|
||||
data: { idTask: id, idUser: userMobile.id, status: 0 }
|
||||
}),
|
||||
prisma.divisionProjectTask.update({
|
||||
where: { id },
|
||||
data: { status: 2 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateTaskStatus(task.idProject);
|
||||
|
||||
const approverTargets = await getApproversForDivision(String(userMobile.idVillage), task.idDivision);
|
||||
await sendNotification({
|
||||
targets: approverTargets,
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.idProject,
|
||||
category: `division/${task.idDivision}/task`,
|
||||
title: 'Pengajuan Penyelesaian Tugas',
|
||||
desc: task.title,
|
||||
});
|
||||
|
||||
await createLogUserMobile({ act: 'CREATE', desc: 'User mengajukan task divisi untuk persetujuan', table: 'divisionProjectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil diajukan untuk persetujuan" }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mengajukan tugas (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PUT — Setujui atau Tolak
|
||||
export async function PUT(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { user, action, note } = await request.json();
|
||||
|
||||
if (!['approve', 'reject'].includes(action)) {
|
||||
return NextResponse.json({ success: false, message: "Action tidak valid, gunakan 'approve' atau 'reject'" }, { status: 200 });
|
||||
}
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id) {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const taskForAuth = await prisma.divisionProjectTask.findUnique({
|
||||
where: { id, isActive: true },
|
||||
select: { idDivision: true }
|
||||
});
|
||||
if (!taskForAuth) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const [division, userFull, isDivAdmin] = await Promise.all([
|
||||
prisma.division.findUnique({
|
||||
where: { id: taskForAuth.idDivision },
|
||||
select: { idGroup: true, idVillage: true }
|
||||
}),
|
||||
prisma.user.findUnique({
|
||||
where: { id: userMobile.id },
|
||||
select: { isApprover: true, idGroup: true, idVillage: true, UserRole: { select: { id: true } } }
|
||||
}),
|
||||
prisma.divisionMember.count({
|
||||
where: { idDivision: taskForAuth.idDivision, idUser: userMobile.id, isAdmin: true, isActive: true }
|
||||
})
|
||||
]);
|
||||
|
||||
const isSupadmin = APPROVER_ROLES.includes(userFull?.UserRole?.id ?? '');
|
||||
const isGroupApprover = !!(userFull?.isApprover &&
|
||||
userFull.idVillage === division?.idVillage &&
|
||||
userFull.idGroup === division?.idGroup);
|
||||
|
||||
if (!isSupadmin && !isGroupApprover && isDivAdmin === 0) {
|
||||
return NextResponse.json({ success: false, message: "Anda tidak memiliki izin untuk menyetujui atau menolak tugas" }, { status: 200 });
|
||||
}
|
||||
|
||||
const task = await prisma.divisionProjectTask.findUnique({
|
||||
where: { id, isActive: true },
|
||||
select: { id: true, status: true, idProject: true, idDivision: true, title: true }
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (task.status !== 2) {
|
||||
return NextResponse.json({ success: false, message: "Tugas tidak sedang menunggu persetujuan" }, { status: 200 });
|
||||
}
|
||||
|
||||
const pendingApproval = await prisma.divisionProjectTaskApproval.findFirst({
|
||||
where: { idTask: id, status: 0 },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, idUser: true }
|
||||
});
|
||||
|
||||
if (!pendingApproval) {
|
||||
return NextResponse.json({ success: false, message: "Data persetujuan pending tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (action === 'approve') {
|
||||
await prisma.$transaction([
|
||||
prisma.divisionProjectTaskApproval.update({
|
||||
where: { id: pendingApproval.id },
|
||||
data: { status: 1, idApprover: userMobile.id }
|
||||
}),
|
||||
prisma.divisionProjectTask.update({
|
||||
where: { id },
|
||||
data: { status: 1 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateTaskStatus(task.idProject);
|
||||
|
||||
const [submitterTarget, approverTargets] = await Promise.all([
|
||||
getUserNotifTarget(pendingApproval.idUser),
|
||||
getApproversForDivision(String(userMobile.idVillage), task.idDivision),
|
||||
]);
|
||||
const notifTargets = _.uniqBy([
|
||||
...(submitterTarget ? [submitterTarget] : []),
|
||||
...approverTargets,
|
||||
], 'idUserTo');
|
||||
await sendNotification({
|
||||
targets: notifTargets,
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.idProject,
|
||||
category: `division/${task.idDivision}/task`,
|
||||
title: 'Tugas Disetujui',
|
||||
desc: task.title,
|
||||
});
|
||||
|
||||
await createLogUserMobile({ act: 'UPDATE', desc: 'Approver menyetujui task divisi', table: 'divisionProjectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil disetujui" }, { status: 200 });
|
||||
}
|
||||
|
||||
if (!note || String(note).trim() === '') {
|
||||
return NextResponse.json({ success: false, message: "Alasan penolakan wajib diisi" }, { status: 200 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.divisionProjectTaskApproval.update({
|
||||
where: { id: pendingApproval.id },
|
||||
data: { status: 2, idApprover: userMobile.id, note: String(note).trim() }
|
||||
}),
|
||||
prisma.divisionProjectTask.update({
|
||||
where: { id },
|
||||
data: { status: 0 }
|
||||
})
|
||||
]);
|
||||
|
||||
await recalculateTaskStatus(task.idProject);
|
||||
|
||||
const [submitterTarget, approverTargets] = await Promise.all([
|
||||
getUserNotifTarget(pendingApproval.idUser),
|
||||
getApproversForDivision(String(userMobile.idVillage), task.idDivision),
|
||||
]);
|
||||
const notifTargets = _.uniqBy([
|
||||
...(submitterTarget ? [submitterTarget] : []),
|
||||
...approverTargets,
|
||||
], 'idUserTo');
|
||||
await sendNotification({
|
||||
targets: notifTargets,
|
||||
idUserFrom: userMobile.id,
|
||||
idContent: task.idProject,
|
||||
category: `division/${task.idDivision}/task`,
|
||||
title: 'Tugas Ditolak',
|
||||
desc: task.title,
|
||||
});
|
||||
|
||||
await createLogUserMobile({ act: 'UPDATE', desc: 'Approver menolak task divisi', table: 'divisionProjectTaskApproval', data: id, user: userMobile.id });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Tugas berhasil ditolak" }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal memproses persetujuan (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export async function GET(request: Request, context: { params: { id: string } })
|
||||
img: true,
|
||||
idGroup: true,
|
||||
isActive: true,
|
||||
isApprover: true,
|
||||
idPosition: true,
|
||||
UserRole: {
|
||||
select: {
|
||||
@@ -140,37 +139,6 @@ export async function DELETE(request: Request, context: { params: { id: string }
|
||||
}
|
||||
|
||||
|
||||
// TOGGLE APPROVER STATUS
|
||||
export async function PATCH(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = context.params;
|
||||
const { user, isApprover } = await request.json();
|
||||
|
||||
if (user == "null" || user == undefined || user == "") {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const data = await prisma.user.count({ where: { id } });
|
||||
if (data == 0) {
|
||||
return NextResponse.json({ success: false, message: "Anggota tidak ditemukan" }, { status: 200 });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { isApprover: Boolean(isApprover) },
|
||||
});
|
||||
|
||||
const log = await createLogUserMobile({ act: 'UPDATE', desc: 'User mengupdate status approver anggota', table: 'user', data: id, user });
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mengupdate status approver anggota" }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mengupdate status approver, coba lagi nanti (error: 500)", reason: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UPDATE MEMBER
|
||||
export async function PUT(request: Request, context: { params: { id: string } }) {
|
||||
try {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import { funGetUserById } from "@/module/auth";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET indicator dot per bulan (tanggal mana saja yang ada event)
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const user = searchParams.get("user");
|
||||
const date = searchParams.get("date");
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id || userMobile.id === "null") {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const idVillage = userMobile.idVillage;
|
||||
const awalDate = moment(date).format("YYYY-MM") + "-01";
|
||||
const akhirDate = moment(awalDate).add(1, "M").format("YYYY-MM-DD");
|
||||
|
||||
// Tanggal dengan event kalender divisi
|
||||
const calendarDates = await prisma.divisionCalendarReminder.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
dateStart: {
|
||||
gte: new Date(awalDate),
|
||||
lt: new Date(akhirDate),
|
||||
},
|
||||
Division: {
|
||||
idVillage,
|
||||
isActive: true,
|
||||
},
|
||||
DivisionCalendar: {
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
select: { dateStart: true },
|
||||
});
|
||||
|
||||
// Tanggal dengan task project se-village (ambil semua task yang overlap dengan bulan ini)
|
||||
const taskDates = await prisma.projectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
dateStart: { lt: new Date(akhirDate) },
|
||||
dateEnd: { gte: new Date(awalDate) },
|
||||
Project: {
|
||||
isActive: true,
|
||||
idVillage: String(idVillage),
|
||||
},
|
||||
},
|
||||
select: { dateStart: true, dateEnd: true },
|
||||
});
|
||||
|
||||
// Expand setiap task menjadi array tanggal per hari dalam range bulan
|
||||
const taskExpandedDates: string[] = [];
|
||||
for (const task of taskDates) {
|
||||
let cur = moment(task.dateStart);
|
||||
const end = moment(task.dateEnd);
|
||||
const monthStart = moment(awalDate);
|
||||
const monthEnd = moment(akhirDate);
|
||||
while (cur.isSameOrBefore(end)) {
|
||||
if (cur.isSameOrAfter(monthStart) && cur.isBefore(monthEnd)) {
|
||||
taskExpandedDates.push(cur.format("YYYY-MM-DD"));
|
||||
}
|
||||
cur.add(1, 'day');
|
||||
}
|
||||
}
|
||||
|
||||
const calendarResult = _.uniq(calendarDates.map((v) => moment(v.dateStart).format("YYYY-MM-DD"))).sort();
|
||||
const taskResult = _.uniq(taskExpandedDates).sort();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Berhasil mendapatkan indicator kalender",
|
||||
data: { calendar: calendarResult, task: taskResult }
|
||||
}, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan indicator kalender (error: 500)" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
import { funGetUserById } from "@/module/auth";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import "moment/locale/id";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// GET events per tanggal (DivisionCalendarReminder + ProjectTask)
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const user = searchParams.get("user");
|
||||
const date = searchParams.get("date");
|
||||
|
||||
const userMobile = await funGetUserById({ id: String(user) });
|
||||
if (!userMobile.id || userMobile.id === "null") {
|
||||
return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 200 });
|
||||
}
|
||||
|
||||
const targetDate = new Date(String(date));
|
||||
const idVillage = userMobile.idVillage;
|
||||
|
||||
// Ambil semua event kalender divisi se-village pada tanggal tersebut
|
||||
const calendarData = await prisma.divisionCalendarReminder.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
dateStart: targetDate,
|
||||
Division: {
|
||||
idVillage,
|
||||
isActive: true,
|
||||
},
|
||||
DivisionCalendar: {
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idCalendar: true,
|
||||
dateStart: true,
|
||||
dateEnd: true,
|
||||
timeStart: true,
|
||||
timeEnd: true,
|
||||
Division: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
DivisionCalendar: {
|
||||
select: {
|
||||
title: true,
|
||||
desc: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ timeStart: "asc" }, { timeEnd: "asc" }],
|
||||
});
|
||||
|
||||
// Ambil semua task project se-village yang mencakup tanggal tersebut (dateStart <= target <= dateEnd)
|
||||
const taskData = await prisma.projectTask.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
dateStart: { lte: targetDate },
|
||||
dateEnd: { gte: targetDate },
|
||||
Project: {
|
||||
isActive: true,
|
||||
idVillage: String(idVillage),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
desc: true,
|
||||
dateStart: true,
|
||||
dateEnd: true,
|
||||
Project: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { dateStart: "asc" },
|
||||
});
|
||||
|
||||
const calendarResult = calendarData.map((v) => ({
|
||||
id: v.id,
|
||||
type: "calendar",
|
||||
title: v.DivisionCalendar.title,
|
||||
desc: v.DivisionCalendar.desc,
|
||||
dateStart: moment(v.dateStart).format("YYYY-MM-DD"),
|
||||
dateEnd: v.dateEnd ? moment(v.dateEnd).format("YYYY-MM-DD") : null,
|
||||
timeStart: moment.utc(v.timeStart).format("HH:mm"),
|
||||
timeEnd: moment.utc(v.timeEnd).format("HH:mm"),
|
||||
divisionName: v.Division.name,
|
||||
projectName: null,
|
||||
idDivision: v.Division.id,
|
||||
idRef: v.Division.id,
|
||||
}));
|
||||
|
||||
const taskResult = taskData.map((v) => ({
|
||||
id: v.id,
|
||||
type: "task",
|
||||
title: v.title,
|
||||
desc: v.desc ?? "",
|
||||
dateStart: moment(v.dateStart).format("YYYY-MM-DD"),
|
||||
dateEnd: moment(v.dateEnd).format("YYYY-MM-DD"),
|
||||
timeStart: null,
|
||||
timeEnd: null,
|
||||
divisionName: null,
|
||||
projectName: v.Project.title,
|
||||
idRef: v.Project.id,
|
||||
}));
|
||||
|
||||
const data = [...calendarResult, ...taskResult];
|
||||
|
||||
return NextResponse.json({ success: true, message: "Berhasil mendapatkan kalender umum", data }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ success: false, message: "Gagal mendapatkan kalender umum (error: 500)" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
import { isValidApiKey } from "@/lib/apiKey";
|
||||
import { prisma } from "@/module/_global";
|
||||
import cors from "@elysiajs/cors";
|
||||
import { swagger } from "@elysiajs/swagger";
|
||||
@@ -12,40 +11,20 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
||||
.use(cors({
|
||||
origin: "*",
|
||||
methods: ["GET", "POST", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "x-api-key"],
|
||||
}))
|
||||
.use(swagger({
|
||||
path: "/docs",
|
||||
path: "/docs", // Karena prefix instance adalah /api/noc, maka ini akan diakses di /api/noc/docs
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Sistem Desa Mandiri - NOC API",
|
||||
version: "1.0.0",
|
||||
description: "API Khusus untuk kebutuhan NOC (Network Operation Center) dan Monitoring Desa",
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: "apiKey",
|
||||
in: "header",
|
||||
name: "x-api-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
tags: [
|
||||
{ name: "NOC", description: "Endpoint khusus monitoring" }
|
||||
]
|
||||
}
|
||||
}))
|
||||
.onBeforeHandle(async ({ request, set, path }) => {
|
||||
if (path.startsWith("/api/noc/docs")) return;
|
||||
|
||||
const incoming = request.headers.get("x-api-key");
|
||||
if (!incoming || !(await isValidApiKey(incoming))) {
|
||||
set.status = 401;
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
})
|
||||
|
||||
// ── GET /api/noc/active-divisions ──────────────────────────────────────────
|
||||
.get(
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { prisma } from "@/module/_global";
|
||||
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
let apiKeyCache: Set<string> = new Set();
|
||||
let cacheExpiresAt = 0;
|
||||
|
||||
export async function isValidApiKey(incoming: string): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
if (now > cacheExpiresAt) {
|
||||
const rows = await prisma.apiKey.findMany({ where: { isActive: true }, select: { key: true } });
|
||||
apiKeyCache = new Set(rows.map((r) => r.key));
|
||||
cacheExpiresAt = now + CACHE_TTL_MS;
|
||||
}
|
||||
return apiKeyCache.has(incoming);
|
||||
}
|
||||
Reference in New Issue
Block a user