Compare commits
16 Commits
tasks/refa
...
faf78064c7
| Author | SHA1 | Date | |
|---|---|---|---|
| faf78064c7 | |||
| 9dd5d1545f | |||
| a4c7a97593 | |||
| 5ab014281a | |||
| 865074a310 | |||
| b640bb3919 | |||
| f48b982b3c | |||
| cfe06137d8 | |||
| f0504c9dc0 | |||
| 1916c616de | |||
| e3345c71f5 | |||
| 68da360cea | |||
| b9b2b65294 | |||
| 71e23dea1a | |||
| cd7425292c | |||
| 187e3a2115 |
42
.claude/ARCHITECTURE.md
Normal file
42
.claude/ARCHITECTURE.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Framework**: Next.js 15 (App Router) + React 19
|
||||||
|
- **Runtime/Package manager**: Bun (not npm)
|
||||||
|
- **API server**: Elysia.js (mounted at `/api/[[...slugs]]`)
|
||||||
|
- **ORM**: Prisma + PostgreSQL
|
||||||
|
- **UI**: Mantine UI v7-8
|
||||||
|
- **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching)
|
||||||
|
- **Auth**: iron-session + JWT
|
||||||
|
- **File storage**: Local uploads + Seafile (self-hosted)
|
||||||
|
|
||||||
|
## Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser → Next.js middleware (src/middleware.ts)
|
||||||
|
→ Public pages: src/app/darmasaba/
|
||||||
|
→ Admin pages: src/app/admin/
|
||||||
|
→ API: src/app/api/[[...slugs]]/route.ts (Elysia.js)
|
||||||
|
└── _lib/*.ts (domain modules)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
|
||||||
|
|
||||||
|
## Domain Modules
|
||||||
|
Each domain (desa, kesehatan, ekonomi, etc.) has:
|
||||||
|
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>.ts`
|
||||||
|
- Admin CMS pages in `src/app/admin/(dashboard)/<domain>/`
|
||||||
|
- Public pages in `src/app/darmasaba/(pages)/<domain>/`
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/middleware.ts` | Route guards and auth |
|
||||||
|
| `src/lib/prisma.ts` | Prisma client singleton |
|
||||||
|
| `src/lib/api-auth.ts` | JWT/session validation |
|
||||||
|
| `src/lib/api-fetch.ts` | Typed fetch wrapper used by frontend |
|
||||||
|
| `src/lib/session.ts` | iron-session config |
|
||||||
|
| `next.config.ts` | Next.js config (cache headers, allowed origins) |
|
||||||
|
| `postcss.config.cjs` | Mantine CSS preset and breakpoints |
|
||||||
|
| `docker-entrypoint.sh` | Runs `prisma migrate deploy` then starts app |
|
||||||
16
.claude/DATABASE.md
Normal file
16
.claude/DATABASE.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Database & Data Layer
|
||||||
|
|
||||||
|
## Prisma Schema
|
||||||
|
- Schema at `prisma/schema.prisma` (~2400 lines, 100+ models)
|
||||||
|
- Common model conventions: `@default(cuid())` IDs, `createdAt`/`updatedAt` timestamps, `deletedAt DateTime?` (soft delete), `isActive Boolean @default(true)`
|
||||||
|
- Seeders per-module in `prisma/_seeder_list/`, orchestrated by `prisma/seed.ts`
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
1. User submits phone → OTP sent (email/SMS)
|
||||||
|
2. OTP validated → JWT created + iron-session stored
|
||||||
|
3. `UserSession` model tracks active sessions
|
||||||
|
4. `src/middleware.ts` validates on each request
|
||||||
|
5. `src/lib/api-auth.ts` handles JWT/session checks in API routes
|
||||||
|
|
||||||
|
## File Handling
|
||||||
|
All uploaded files reference the `FileStorage` Prisma model. Uploads land in `WIBU_UPLOAD_DIR` (default: `uploads/`). Seafile is the external storage fallback.
|
||||||
34
.claude/DEPLOYMENT.md
Normal file
34
.claude/DEPLOYMENT.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env`. Required variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://..."
|
||||||
|
NEXT_PUBLIC_BASE_URL="/"
|
||||||
|
BASE_SESSION_KEY="..." # random string
|
||||||
|
BASE_TOKEN_KEY="..." # random string
|
||||||
|
SESSION_PASSWORD="..." # min 32 chars
|
||||||
|
SEAFILE_TOKEN="..."
|
||||||
|
SEAFILE_REPO_ID="..."
|
||||||
|
SEAFILE_URL="..."
|
||||||
|
MINIO_ENDPOINT="..."
|
||||||
|
MINIO_ACCESS_KEY="..."
|
||||||
|
MINIO_SECRET_KEY="..."
|
||||||
|
MINIO_BUCKET="..."
|
||||||
|
MINIO_USE_SSL="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Multi-stage build: `oven/bun:1-debian` → builder → runner. The runner creates a `nextjs` user (UID 1001), exposes port 3000, and mounts `/app/uploads` as a volume. Entrypoint runs migrations automatically.
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
GitHub Actions workflows in `.github/workflows/`:
|
||||||
|
- `docker-publish.yml` — triggers on `v*` tags, pushes to GHCR
|
||||||
|
- `publish.yml` — manual build & push
|
||||||
|
- `re-pull.yml` — triggers Portainer to redeploy latest image
|
||||||
|
|
||||||
|
To release: tag with `git tag -a v0.1.x -m "..."` and push the tag.
|
||||||
120
.claude/commands/deploy-stg.md
Normal file
120
.claude/commands/deploy-stg.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# deploy-stg
|
||||||
|
|
||||||
|
Deploy ke staging environment secara penuh menggunakan MCP server `deploy-stg`.
|
||||||
|
|
||||||
|
**Repo GitHub:** `bipprojectbali/desa-darmasaba`
|
||||||
|
**Branch stg:** `stg`
|
||||||
|
**STG URL:** `https://desa-darmasaba-stg.wibudev.com`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alur Eksekusi
|
||||||
|
|
||||||
|
### Langkah 0 — Cek /api/version endpoint
|
||||||
|
Pastikan endpoint `/api/version` sudah ada di API:
|
||||||
|
```bash
|
||||||
|
grep -n '"/version"' src/app/api/\[\[...slugs\]\]/route.ts
|
||||||
|
```
|
||||||
|
Jika belum ada, tambahkan ke main API group yang membaca `version` dari `package.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 1 — Version Bump
|
||||||
|
|
||||||
|
Gunakan MCP tool `bump_version` (server: `deploy-stg`).
|
||||||
|
|
||||||
|
Tool otomatis baca `package.json`, increment patch (+1), tulis kembali.
|
||||||
|
Catat `new_version` dari response — akan dipakai di Langkah 5 dan 6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 2 — Cek & Buat Migration
|
||||||
|
|
||||||
|
Gunakan MCP tool `check_migrations` (server: `deploy-stg`).
|
||||||
|
|
||||||
|
- Jika `needs_migration: true` → jalankan `create_migration` dengan `name: bump-stg-<new_version>`
|
||||||
|
- Jika `is_up_to_date: true` → lanjut ke Langkah 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 3 — Build Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Jika build gagal → **stop**, perbaiki error sebelum melanjutkan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 4 — Commit & Push ke stg
|
||||||
|
|
||||||
|
Gunakan MCP tool `commit_and_push_stg` (server: `deploy-stg`).
|
||||||
|
|
||||||
|
Tool otomatis stage `package.json` + `prisma/migrations/`, commit dengan message yang menyertakan versi baru, lalu push ke branch `stg` menggunakan `GH_TOKEN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 5 — Trigger publish.yml
|
||||||
|
|
||||||
|
Gunakan MCP tool `trigger_publish` (server: `deploy-stg`):
|
||||||
|
- `stack_env`: `stg` (default)
|
||||||
|
- `tag`: nilai `new_version` dari Langkah 1 (sama persis dengan versi di `package.json`)
|
||||||
|
|
||||||
|
Tool mengembalikan `run_id`. Poll dengan `watch_workflow_run` setiap 30 detik hingga `status == "completed"`:
|
||||||
|
- `conclusion == "success"` → lanjut ke Langkah 6
|
||||||
|
- `conclusion != "success"` → **stop**, tampilkan error, jangan lanjut ke re-pull
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 6 — Trigger re-pull.yml
|
||||||
|
|
||||||
|
Setelah publish berhasil, gunakan MCP tool `trigger_repull` (server: `deploy-stg`):
|
||||||
|
- `stack_name`: otomatis dari env `STACK_NAME` (tidak perlu diisi manual)
|
||||||
|
- `stack_env`: `stg`
|
||||||
|
|
||||||
|
Stack yang di-deploy: `<STACK_NAME>-stg`. Poll dengan `watch_workflow_run` setiap 30 detik hingga `status == "completed"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Langkah 7 — Verifikasi Versi
|
||||||
|
|
||||||
|
Gunakan MCP tool `check_stg_version` (server: `deploy-stg`):
|
||||||
|
- `wait_seconds`: `30` (tunggu container siap)
|
||||||
|
|
||||||
|
Tool otomatis fetch `BASE_URL/api/version` dan bandingkan dengan versi lokal.
|
||||||
|
- `match: true` → **Deploy berhasil!**
|
||||||
|
- `match: false` → cek container logs di Portainer atau jalankan `gh run view <run_id> --repo bipprojectbali/desa-darmasaba --log`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ringkasan MCP Tools
|
||||||
|
|
||||||
|
| Tool | Server | Tujuan |
|
||||||
|
|------|--------|--------|
|
||||||
|
| `bump_version` | `deploy-stg` | Increment patch version di package.json |
|
||||||
|
| `check_migrations` | `deploy-stg` | Cek status Prisma migrations |
|
||||||
|
| `create_migration` | `deploy-stg` | Buat migration baru jika diperlukan |
|
||||||
|
| `commit_and_push_stg` | `deploy-stg` | Commit + push ke branch stg |
|
||||||
|
| `trigger_publish` | `deploy-stg` | Trigger publish.yml (build Docker image) |
|
||||||
|
| `watch_workflow_run` | `deploy-stg` | Poll status workflow run |
|
||||||
|
| `trigger_repull` | `deploy-stg` | Trigger re-pull.yml (redeploy di Portainer) |
|
||||||
|
| `check_stg_version` | `deploy-stg` | Bandingkan versi lokal vs STG |
|
||||||
|
|
||||||
|
## Ringkasan Workflow Inputs
|
||||||
|
|
||||||
|
| Workflow | Input | Value |
|
||||||
|
|----------|-------|-------|
|
||||||
|
| `publish.yml` | `stack_env` | `stg` |
|
||||||
|
| `publish.yml` | `tag` | versi dari `package.json` (e.g. `0.1.26`) |
|
||||||
|
| `publish.yml` | `stack_name` | dari env `STACK_NAME` (opsional) |
|
||||||
|
| `re-pull.yml` | `stack_name` | dari env `STACK_NAME` (otomatis) |
|
||||||
|
| `re-pull.yml` | `stack_env` | `stg` |
|
||||||
|
|
||||||
|
## Catatan
|
||||||
|
|
||||||
|
- Jangan jalankan `re-pull.yml` jika `publish.yml` belum selesai/berhasil.
|
||||||
|
- Isi `GH_TOKEN` di `.env` sebelum menjalankan deploy (bisa sama dengan `GH_TOKEN`).
|
||||||
|
- `BASE_URL` di `.env` sudah diset ke `https://desa-darmasaba-stg.wibudev.com`.
|
||||||
|
- `STACK_NAME` di `.env` diset ke nama stack (e.g. `desa-darmasaba`) — dipakai otomatis oleh `trigger_publish` dan `trigger_repull`.
|
||||||
|
- Verifikasi versi via `/api/version` (bukan `/api/utils/version`).
|
||||||
478
.claude/mcp/github-actions.mjs
Normal file
478
.claude/mcp/github-actions.mjs
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Server: GitHub Actions Workflow Tools + Deploy-STG Pre/Post Steps
|
||||||
|
* Tools: trigger_publish, trigger_repull, get_workflow_runs, watch_workflow_run,
|
||||||
|
* check_migrations, create_migration, bump_version, commit_and_push_stg, check_stg_version
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const REPO = "bipprojectbali/desa-darmasaba";
|
||||||
|
const CWD = process.cwd();
|
||||||
|
|
||||||
|
// --- MCP Protocol Helpers ---
|
||||||
|
|
||||||
|
function send(obj) {
|
||||||
|
process.stdout.write(JSON.stringify(obj) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function respond(id, result) {
|
||||||
|
send({ jsonrpc: "2.0", id, result });
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondError(id, code, message) {
|
||||||
|
send({ jsonrpc: "2.0", id, error: { code, message } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shell Helper ---
|
||||||
|
|
||||||
|
function runCmd(cmd, opts = {}) {
|
||||||
|
const r = spawnSync("sh", ["-c", cmd], {
|
||||||
|
encoding: "utf-8",
|
||||||
|
cwd: CWD,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
if (r.error) return { ok: false, out: r.error.message };
|
||||||
|
if (r.status !== 0) return { ok: false, out: (r.stderr || r.stdout || "").trim() };
|
||||||
|
return { ok: true, out: (r.stdout || "").trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestRunId(workflow, delaySecs = 3) {
|
||||||
|
const r = runCmd(
|
||||||
|
`sleep ${delaySecs} && gh run list --workflow=${workflow} --repo ${REPO} --limit 1 --json databaseId -q '.[0].databaseId'`
|
||||||
|
);
|
||||||
|
return r.ok ? r.out.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tool Definitions ---
|
||||||
|
|
||||||
|
const TOOLS = [
|
||||||
|
{
|
||||||
|
name: "check_migrations",
|
||||||
|
description: "Cek status Prisma migrations. Returns apakah ada pending migrations.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_migration",
|
||||||
|
description: "Buat Prisma migration baru (prisma migrate dev). Jalankan hanya jika check_migrations mendeteksi pending/drift.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama migration (e.g. bump-stg-0.1.26)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bump_version",
|
||||||
|
description: "Baca versi dari package.json, increment patch (+1), tulis kembali. Returns old_version dan new_version.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "commit_and_push_stg",
|
||||||
|
description: "Stage package.json + prisma/migrations, commit, lalu push ke branch stg menggunakan GH_TOKEN.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: "string",
|
||||||
|
description: "Commit message. Jika tidak diisi, auto-generate dari versi package.json.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "check_stg_version",
|
||||||
|
description: "Bandingkan versi lokal (package.json) dengan versi di STG (/api/version). Tunggu container siap jika perlu.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
wait_seconds: {
|
||||||
|
type: "number",
|
||||||
|
description: "Detik tunggu sebelum fetch STG (default: 30, untuk memberi waktu container siap).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trigger_publish",
|
||||||
|
description:
|
||||||
|
"Trigger publish.yml workflow: build & push Docker image ke GHCR. Returns run ID.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
stack_env: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["dev", "stg", "prod"],
|
||||||
|
description: "Target environment. Default: stg.",
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: "string",
|
||||||
|
description: "Image tag — harus sama persis dengan versi di package.json (e.g. 0.1.25).",
|
||||||
|
},
|
||||||
|
stack_name: {
|
||||||
|
type: "string",
|
||||||
|
description: `Nama stack. Default: nilai env STACK_NAME (${process.env.STACK_NAME || "belum diset"}).`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["stack_env", "tag"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trigger_repull",
|
||||||
|
description:
|
||||||
|
"Trigger re-pull.yml workflow: redeploy stack di Portainer. Jalankan HANYA setelah publish berhasil.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
stack_name: {
|
||||||
|
type: "string",
|
||||||
|
description: `Nama stack (e.g. desa-darmasaba). Stack yang di-deploy: <stack_name>-<stack_env>. Default: nilai env STACK_NAME (${process.env.STACK_NAME || "belum diset"}).`,
|
||||||
|
},
|
||||||
|
stack_env: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["dev", "stg", "prod"],
|
||||||
|
description: "Target environment.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["stack_env"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_workflow_runs",
|
||||||
|
description: "List run terbaru dari suatu workflow beserta status dan conclusion-nya.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
workflow: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama file workflow (e.g. publish.yml, re-pull.yml)",
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Jumlah run yang ditampilkan (default: 5)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["workflow"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "watch_workflow_run",
|
||||||
|
description:
|
||||||
|
"Lihat status detail sebuah workflow run: apakah in_progress, success, atau failure.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
run_id: {
|
||||||
|
type: "number",
|
||||||
|
description: "ID workflow run (dapat dari trigger_publish / trigger_repull / get_workflow_runs)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["run_id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Tool Handlers ---
|
||||||
|
|
||||||
|
function handleCheckMigrations() {
|
||||||
|
const r = runCmd("bunx prisma migrate status 2>&1", { timeout: 30000 });
|
||||||
|
const out = r.out || "";
|
||||||
|
const hasPending =
|
||||||
|
out.includes("following migration(s) have not yet been applied") ||
|
||||||
|
out.includes("drift") ||
|
||||||
|
out.includes("pending");
|
||||||
|
const isUpToDate =
|
||||||
|
out.includes("Database schema is up to date") ||
|
||||||
|
out.includes("up to date");
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
needs_migration: hasPending && !isUpToDate,
|
||||||
|
is_up_to_date: isUpToDate,
|
||||||
|
output: out,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateMigration(args) {
|
||||||
|
const { name } = args;
|
||||||
|
const r = runCmd(
|
||||||
|
`echo "" | bunx prisma migrate dev --name ${name} --skip-generate 2>&1`,
|
||||||
|
{ timeout: 120000 }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ok: r.ok,
|
||||||
|
output: r.out,
|
||||||
|
error: r.ok ? undefined : r.out,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBumpVersion() {
|
||||||
|
const pkgPath = join(CWD, "package.json");
|
||||||
|
let pkg;
|
||||||
|
try {
|
||||||
|
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: `Gagal baca package.json: ${e.message}` };
|
||||||
|
}
|
||||||
|
const oldVersion = pkg.version;
|
||||||
|
const [maj, min, pat] = oldVersion.split(".").map(Number);
|
||||||
|
const newVersion = `${maj}.${min}.${pat + 1}`;
|
||||||
|
pkg.version = newVersion;
|
||||||
|
try {
|
||||||
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: `Gagal tulis package.json: ${e.message}` };
|
||||||
|
}
|
||||||
|
return { ok: true, old_version: oldVersion, new_version: newVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommitAndPushStg(args) {
|
||||||
|
const { message } = args || {};
|
||||||
|
|
||||||
|
// Baca versi untuk auto commit message
|
||||||
|
let version = "unknown";
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf-8"));
|
||||||
|
version = pkg.version;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const commitMsg = message || `chore: bump version to ${version} for stg deploy`;
|
||||||
|
|
||||||
|
// Stage file
|
||||||
|
runCmd("git add package.json 2>&1");
|
||||||
|
runCmd("git add prisma/migrations/ 2>&1");
|
||||||
|
|
||||||
|
// Cek apakah ada yang di-stage
|
||||||
|
const statusR = runCmd("git diff --cached --name-only 2>&1");
|
||||||
|
if (!statusR.out.trim()) {
|
||||||
|
return { ok: false, error: "Tidak ada perubahan yang di-stage untuk di-commit." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit
|
||||||
|
const commitR = runCmd(`git commit -m "${commitMsg}" 2>&1`);
|
||||||
|
if (!commitR.ok) return { ok: false, step: "commit", error: commitR.out };
|
||||||
|
|
||||||
|
// Push ke stg menggunakan GH_TOKEN jika tersedia
|
||||||
|
const token = process.env.GH_TOKEN;
|
||||||
|
const pushCmd = token
|
||||||
|
? `git push https://x-access-token:${token}@github.com/${REPO}.git HEAD:stg 2>&1`
|
||||||
|
: `git push origin HEAD:stg 2>&1`;
|
||||||
|
const pushR = runCmd(pushCmd, { timeout: 60000 });
|
||||||
|
if (!pushR.ok) return { ok: false, step: "push", error: pushR.out };
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
message: `Berhasil commit "${commitMsg}" dan push ke stg`,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckStgVersion(args) {
|
||||||
|
const waitSecs = (args && args.wait_seconds) || 30;
|
||||||
|
const baseUrl = process.env.BASE_URL;
|
||||||
|
if (!baseUrl) {
|
||||||
|
return { ok: false, error: "BASE_URL tidak ada di environment — isi di .env" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baca versi lokal
|
||||||
|
let localVersion;
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf-8"));
|
||||||
|
localVersion = pkg.version;
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: `Gagal baca package.json: ${e.message}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tunggu container siap
|
||||||
|
if (waitSecs > 0) {
|
||||||
|
runCmd(`sleep ${waitSecs}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch versi dari STG
|
||||||
|
const r = runCmd(`curl -sf --max-time 10 "${baseUrl}/api/version" 2>&1`);
|
||||||
|
if (!r.ok) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Gagal fetch ${baseUrl}/api/version: ${r.out}`,
|
||||||
|
local_version: localVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let stgVersion;
|
||||||
|
try {
|
||||||
|
stgVersion = JSON.parse(r.out).version;
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Gagal parse response: ${r.out}`,
|
||||||
|
local_version: localVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = localVersion === stgVersion;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
local_version: localVersion,
|
||||||
|
stg_version: stgVersion,
|
||||||
|
match,
|
||||||
|
status: match
|
||||||
|
? "✓ DEPLOY BERHASIL — versi STG sudah sesuai"
|
||||||
|
: "✗ VERSI BERBEDA — cek container logs di Portainer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTriggerPublish(args) {
|
||||||
|
const { stack_env = "stg", tag, stack_name } = args;
|
||||||
|
const resolvedStackName = stack_name || process.env.STACK_NAME;
|
||||||
|
let triggerCmd = `gh workflow run publish.yml --repo ${REPO} --ref ${stack_env} -f stack_env=${stack_env} -f tag=${tag}`;
|
||||||
|
if (resolvedStackName) triggerCmd += ` -f stack_name=${resolvedStackName}`;
|
||||||
|
const r = runCmd(triggerCmd);
|
||||||
|
if (!r.ok) return { ok: false, error: r.out };
|
||||||
|
|
||||||
|
const runId = getLatestRunId("publish.yml");
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
message: `Workflow publish.yml di-trigger (env=${stack_env}, tag=${tag}${resolvedStackName ? `, stack=${resolvedStackName}` : ""})`,
|
||||||
|
run_id: runId ? Number(runId) : null,
|
||||||
|
monitor_hint: runId
|
||||||
|
? `Gunakan watch_workflow_run dengan run_id: ${runId}`
|
||||||
|
: "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTriggerRepull(args) {
|
||||||
|
const resolvedStackName = args.stack_name || process.env.STACK_NAME;
|
||||||
|
const { stack_env } = args;
|
||||||
|
if (!resolvedStackName) {
|
||||||
|
return { ok: false, error: "stack_name wajib diisi atau set env STACK_NAME di .mcp.json" };
|
||||||
|
}
|
||||||
|
const stack_name = resolvedStackName;
|
||||||
|
const triggerCmd = `gh workflow run re-pull.yml --repo ${REPO} --ref main -f stack_name=${stack_name} -f stack_env=${stack_env}`;
|
||||||
|
const r = runCmd(triggerCmd);
|
||||||
|
if (!r.ok) return { ok: false, error: r.out };
|
||||||
|
|
||||||
|
const runId = getLatestRunId("re-pull.yml");
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
message: `Workflow re-pull.yml di-trigger (stack=${stack_name}-${stack_env})`,
|
||||||
|
run_id: runId ? Number(runId) : null,
|
||||||
|
monitor_hint: runId
|
||||||
|
? `Gunakan watch_workflow_run dengan run_id: ${runId}`
|
||||||
|
: "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGetWorkflowRuns(args) {
|
||||||
|
const limit = args.limit || 5;
|
||||||
|
const r = runCmd(
|
||||||
|
`gh run list --workflow=${args.workflow} --repo ${REPO} --limit ${limit} --json databaseId,status,conclusion,displayTitle,createdAt,headBranch`
|
||||||
|
);
|
||||||
|
if (!r.ok) return { ok: false, error: r.out };
|
||||||
|
try {
|
||||||
|
return { ok: true, runs: JSON.parse(r.out) };
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: "Gagal parse output", raw: r.out };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWatchWorkflowRun(args) {
|
||||||
|
const r = runCmd(
|
||||||
|
`gh run view ${args.run_id} --repo ${REPO} --json status,conclusion,displayTitle,headBranch,createdAt,updatedAt,jobs`
|
||||||
|
);
|
||||||
|
if (!r.ok) return { ok: false, error: r.out };
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(r.out);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
run_id: args.run_id,
|
||||||
|
status: data.status,
|
||||||
|
conclusion: data.conclusion,
|
||||||
|
title: data.displayTitle,
|
||||||
|
branch: data.headBranch,
|
||||||
|
jobs: (data.jobs || []).map((j) => ({
|
||||||
|
name: j.name,
|
||||||
|
status: j.status,
|
||||||
|
conclusion: j.conclusion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: "Gagal parse output", raw: r.out };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Request Router ---
|
||||||
|
|
||||||
|
function handleMessage(msg) {
|
||||||
|
const { id, method, params } = msg;
|
||||||
|
|
||||||
|
if (method === "initialize") {
|
||||||
|
respond(id, {
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo: { name: "deploy-stg", version: "2.0.0" },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "notifications/initialized") return;
|
||||||
|
|
||||||
|
if (method === "ping") {
|
||||||
|
respond(id, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "tools/list") {
|
||||||
|
respond(id, { tools: TOOLS });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "tools/call") {
|
||||||
|
const { name, arguments: args = {} } = params;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (name === "check_migrations") result = handleCheckMigrations();
|
||||||
|
else if (name === "create_migration") result = handleCreateMigration(args);
|
||||||
|
else if (name === "bump_version") result = handleBumpVersion();
|
||||||
|
else if (name === "commit_and_push_stg") result = handleCommitAndPushStg(args);
|
||||||
|
else if (name === "check_stg_version") result = handleCheckStgVersion(args);
|
||||||
|
else if (name === "trigger_publish") result = handleTriggerPublish(args);
|
||||||
|
else if (name === "trigger_repull") result = handleTriggerRepull(args);
|
||||||
|
else if (name === "get_workflow_runs") result = handleGetWorkflowRuns(args);
|
||||||
|
else if (name === "watch_workflow_run") result = handleWatchWorkflowRun(args);
|
||||||
|
else {
|
||||||
|
respondError(id, -32601, `Tool tidak ditemukan: ${name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(id, {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
respondError(id, -32601, `Method tidak dikenal: ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Stdin Loop ---
|
||||||
|
|
||||||
|
let buf = "";
|
||||||
|
process.stdin.setEncoding("utf-8");
|
||||||
|
process.stdin.on("data", (chunk) => {
|
||||||
|
buf += chunk;
|
||||||
|
const lines = buf.split("\n");
|
||||||
|
buf = lines.pop();
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
try {
|
||||||
|
handleMessage(JSON.parse(trimmed));
|
||||||
|
} catch {
|
||||||
|
// ignore malformed JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -40,5 +40,12 @@ SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
|||||||
# ElevenLabs API Key (for TTS features - optional)
|
# ElevenLabs API Key (for TTS features - optional)
|
||||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||||
|
|
||||||
|
# MinIO Configuration (Object Storage)
|
||||||
|
MINIO_ENDPOINT=localhost
|
||||||
|
MINIO_ACCESS_KEY=your_minio_access_key
|
||||||
|
MINIO_SECRET_KEY=your_minio_secret_key
|
||||||
|
MINIO_BUCKET=desa-darmasaba
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
|
||||||
# Environment (optional, defaults to development)
|
# Environment (optional, defaults to development)
|
||||||
# NODE_ENV=development
|
# NODE_ENV=development
|
||||||
|
|||||||
13
.mcp.json
Normal file
13
.mcp.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"deploy-stg": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["--env-file=.env", ".claude/mcp/github-actions.mjs"],
|
||||||
|
"env": {
|
||||||
|
"GH_TOKEN": "${GH_TOKEN}",
|
||||||
|
"BASE_URL": "${BASE_URL}",
|
||||||
|
"STACK_NAME": "${STACK_NAME}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
CLAUDE.md
98
CLAUDE.md
@@ -1,10 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
Desa Darmasaba adalah platform manajemen desa digital untuk Desa Darmasaba, Badung, Bali. Melayani website publik (`/darmasaba/*`) dan admin CMS (`/admin/*`).
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Desa Darmasaba is a full-stack digital village management platform for a village in Badung, Bali. It serves both a public-facing website (`/darmasaba/*`) and an admin CMS (`/admin/*`).
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -20,7 +16,7 @@ bun run test:api # Unit tests (Vitest)
|
|||||||
bun run test:e2e # E2E tests (Playwright)
|
bun run test:e2e # E2E tests (Playwright)
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
bunx prisma migrate deploy # Apply migrations
|
bunx prisma migrate deploy # Apply migrations
|
||||||
bunx prisma migrate dev --name <name> # Create migration
|
bunx prisma migrate dev --name <name> # Create migration
|
||||||
bun run prisma/seed.ts # Seed database
|
bun run prisma/seed.ts # Seed database
|
||||||
bunx prisma studio # Interactive DB viewer
|
bunx prisma studio # Interactive DB viewer
|
||||||
@@ -29,91 +25,11 @@ bunx prisma studio # Interactive DB viewer
|
|||||||
bun eslint . --fix
|
bun eslint . --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Reference Docs
|
||||||
|
|
||||||
### Tech Stack
|
- Architecture, request flow, domain modules, key files: @.claude/ARCHITECTURE.md
|
||||||
- **Framework**: Next.js 15 (App Router) + React 19
|
- Database conventions, auth flow, file handling: @.claude/DATABASE.md
|
||||||
- **Runtime/Package manager**: Bun (not npm)
|
- Env vars, Docker, CI/CD, releasing: @.claude/DEPLOYMENT.md
|
||||||
- **API server**: Elysia.js (mounted at `/api/[[...slugs]]`)
|
|
||||||
- **ORM**: Prisma + PostgreSQL
|
|
||||||
- **UI**: Mantine UI v7-8
|
|
||||||
- **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching)
|
|
||||||
- **Auth**: iron-session + JWT
|
|
||||||
- **File storage**: Local uploads + Seafile (self-hosted)
|
|
||||||
|
|
||||||
### Request Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser → Next.js middleware (src/middleware.ts)
|
|
||||||
→ Public pages: src/app/darmasaba/
|
|
||||||
→ Admin pages: src/app/admin/
|
|
||||||
→ API: src/app/api/[[...slugs]]/route.ts (Elysia.js)
|
|
||||||
└── _lib/*.ts (domain modules)
|
|
||||||
```
|
|
||||||
|
|
||||||
The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
|
|
||||||
|
|
||||||
### Domain Modules
|
|
||||||
Each domain (desa, kesehatan, ekonomi, etc.) has:
|
|
||||||
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>.ts`
|
|
||||||
- Admin CMS pages in `src/app/admin/(dashboard)/<domain>/`
|
|
||||||
- Public pages in `src/app/darmasaba/(pages)/<domain>/`
|
|
||||||
|
|
||||||
### Database (Prisma)
|
|
||||||
- Schema at `prisma/schema.prisma` (~2400 lines, 100+ models)
|
|
||||||
- Common model conventions: `@default(cuid())` IDs, `createdAt`/`updatedAt` timestamps, `deletedAt DateTime?` (soft delete), `isActive Boolean @default(true)`
|
|
||||||
- Seeders per-module in `prisma/_seeder_list/`, orchestrated by `prisma/seed.ts`
|
|
||||||
|
|
||||||
### Authentication Flow
|
|
||||||
1. User submits phone → OTP sent (email/SMS)
|
|
||||||
2. OTP validated → JWT created + iron-session stored
|
|
||||||
3. `UserSession` model tracks active sessions
|
|
||||||
4. `src/middleware.ts` validates on each request
|
|
||||||
5. `src/lib/api-auth.ts` handles JWT/session checks in API routes
|
|
||||||
|
|
||||||
### File Handling
|
|
||||||
All uploaded files reference the `FileStorage` Prisma model. Uploads land in `WIBU_UPLOAD_DIR` (default: `uploads/`). Seafile is the external storage fallback.
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `src/middleware.ts` | Route guards and auth |
|
|
||||||
| `src/lib/prisma.ts` | Prisma client singleton |
|
|
||||||
| `src/lib/api-auth.ts` | JWT/session validation |
|
|
||||||
| `src/lib/api-fetch.ts` | Typed fetch wrapper used by frontend |
|
|
||||||
| `src/lib/session.ts` | iron-session config |
|
|
||||||
| `next.config.ts` | Next.js config (cache headers, allowed origins) |
|
|
||||||
| `postcss.config.cjs` | Mantine CSS preset and breakpoints |
|
|
||||||
| `docker-entrypoint.sh` | Runs `prisma migrate deploy` then starts app |
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Copy `.env.example` to `.env`. Required variables:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URL="postgresql://..."
|
|
||||||
NEXT_PUBLIC_BASE_URL="/"
|
|
||||||
BASE_SESSION_KEY="..." # random string
|
|
||||||
BASE_TOKEN_KEY="..." # random string
|
|
||||||
SESSION_PASSWORD="..." # min 32 chars
|
|
||||||
SEAFILE_TOKEN="..."
|
|
||||||
SEAFILE_REPO_ID="..."
|
|
||||||
SEAFILE_URL="..."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
Multi-stage build: `oven/bun:1-debian` → builder → runner. The runner creates a `nextjs` user (UID 1001), exposes port 3000, and mounts `/app/uploads` as a volume. Entrypoint runs migrations automatically.
|
|
||||||
|
|
||||||
## CI/CD
|
|
||||||
|
|
||||||
GitHub Actions workflows in `.github/workflows/`:
|
|
||||||
- `docker-publish.yml` — triggers on `v*` tags, pushes to GHCR
|
|
||||||
- `publish.yml` — manual build & push
|
|
||||||
- `re-pull.yml` — triggers Portainer to redeploy latest image
|
|
||||||
|
|
||||||
To release: tag with `git tag -a v0.1.x -m "..."` and push the tag.
|
|
||||||
|
|
||||||
### Workflow for Code Changes
|
### Workflow for Code Changes
|
||||||
1. **Commit** existing changes before starting new work
|
1. **Commit** existing changes before starting new work
|
||||||
@@ -134,4 +50,4 @@ To release: tag with `git tag -a v0.1.x -m "..."` and push the tag.
|
|||||||
2. **re-pull.yml**: **Wait for `publish.yml` to complete successfully before running.** Uses branch `main`, stack env and stack name `desa-darmasaba`.
|
2. **re-pull.yml**: **Wait for `publish.yml` to complete successfully before running.** Uses branch `main`, stack env and stack name `desa-darmasaba`.
|
||||||
|
|
||||||
### After Progress
|
### After Progress
|
||||||
- Always give option to continue to GitHub workflows or not
|
- Always give option to continue to GitHub workflows or not
|
||||||
@@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY package.json bun.lockb* ./
|
COPY package.json bun.lock* bun.lockb* ./
|
||||||
|
|
||||||
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
|
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|||||||
31
MIND/PLAN/fix-and-improve-umkm-module.md
Normal file
31
MIND/PLAN/fix-and-improve-umkm-module.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Plan: Fix and Improve UMKM Module
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
- Implement delete confirmation modal for UMKM sales histori.
|
||||||
|
- Improve data handling (null safety) in UMKM and Produk forms.
|
||||||
|
- Fix field mapping in Produk edit page.
|
||||||
|
- Enhance UMKM Dashboard KPI calculation.
|
||||||
|
- Implement stock validation for new sales.
|
||||||
|
- Fix WhatsApp link formatting across the application.
|
||||||
|
- Add product ordering functionality for public users.
|
||||||
|
- Translate and simplify CLAUDE.md.
|
||||||
|
|
||||||
|
## Proposed Changes
|
||||||
|
1. **Admin State**: Update `umkmState` to include `del` for sales and refine form schemas.
|
||||||
|
2. **Admin UI**:
|
||||||
|
- Add `ModalKonfirmasiHapus` to Penjualan page.
|
||||||
|
- Fix card height in Dashboard.
|
||||||
|
- Refine Edit/Create pages for UMKM and Produk.
|
||||||
|
3. **API**:
|
||||||
|
- Refactor `kpi.ts` for more accurate reporting.
|
||||||
|
- Add stock check in `create.ts` for sales.
|
||||||
|
4. **Public UI**:
|
||||||
|
- Update Produk detail page with order modal and WhatsApp integration.
|
||||||
|
- Fix WhatsApp links in various pages.
|
||||||
|
5. **Documentation**: Update `CLAUDE.md`.
|
||||||
|
6. **Maintenance**: Increment version in `package.json`.
|
||||||
|
|
||||||
|
## Verification Plan
|
||||||
|
- Run `bun run build` to ensure no compile errors.
|
||||||
|
- Manual verification of delete functionality.
|
||||||
|
- Manual verification of ordering flow.
|
||||||
90
MIND/PLAN/migrate-kategori-produk-umkm.md
Normal file
90
MIND/PLAN/migrate-kategori-produk-umkm.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Plan: Migrasi KategoriProduk → KategoriProdukUmkm untuk UMKM
|
||||||
|
|
||||||
|
## Tujuan
|
||||||
|
Ganti relasi kategori di model `Umkm` dan seluruh CRUD kategori UMKM agar menggunakan model `KategoriProdukUmkm` (bukan `KategoriProduk`). Model `KategoriProduk` tetap dipertahankan untuk `PasarDesa` dan `KategoriToPasar`.
|
||||||
|
|
||||||
|
## Analisis Kondisi Saat Ini
|
||||||
|
|
||||||
|
### Schema Prisma
|
||||||
|
- `Umkm` → `kategori KategoriProduk @relation(...)` + `kategoriId String`
|
||||||
|
- `KategoriProdukUmkm` punya relasi `Umkm[]` tapi Umkm belum punya FK ke sana (schema tidak konsisten)
|
||||||
|
- `KategoriProduk` dipakai oleh: `Umkm[]`, `PasarDesa[]`, `KategoriToPasar[]`
|
||||||
|
|
||||||
|
### File yang Perlu Diubah
|
||||||
|
1. `prisma/schema.prisma` — ubah relasi Umkm
|
||||||
|
2. `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts` — ganti semua `prisma.kategoriProduk` → `prisma.kategoriProdukUmkm`
|
||||||
|
3. `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/create.ts` — relasi kategori di PasarDesa tidak berubah (masih `KategoriProduk`)
|
||||||
|
4. `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/findMany.ts` — include `kategoriProduk` di PasarDesa tidak berubah
|
||||||
|
5. `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts` — groupBy `kategoriId` di Umkm perlu include model baru
|
||||||
|
6. `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts` — state/form kategori UMKM
|
||||||
|
7. Admin pages (list, create, edit) — tidak perlu banyak perubahan (UI tetap sama)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Langkah-Langkah
|
||||||
|
|
||||||
|
### Fase 1 — Schema Prisma
|
||||||
|
**File:** `prisma/schema.prisma`
|
||||||
|
|
||||||
|
1. Di model `Umkm` (line ~2435):
|
||||||
|
- Ubah `kategori KategoriProduk @relation(fields: [kategoriId], references: [id])` → `kategori KategoriProdukUmkm @relation(fields: [kategoriId], references: [id])`
|
||||||
|
- Field `kategoriId String` tetap sama (nama field tidak perlu berubah)
|
||||||
|
|
||||||
|
2. Di model `KategoriProduk` (line ~1453):
|
||||||
|
- Hapus baris `Umkm Umkm[]` (UMKM tidak lagi relasi ke sini)
|
||||||
|
|
||||||
|
3. Model `KategoriProdukUmkm` sudah punya `Umkm Umkm[]` — tidak perlu diubah
|
||||||
|
|
||||||
|
### Fase 2 — Database Migration
|
||||||
|
```bash
|
||||||
|
bunx prisma migrate dev --name migrate-umkm-kategori-to-kategori-produk-umkm
|
||||||
|
```
|
||||||
|
- Migration akan: ubah FK constraint di tabel `Umkm` dari `KategoriProduk` → `KategoriProdukUmkm`
|
||||||
|
- **Perhatian:** Data lama di `kategoriId` merujuk ke `KategoriProduk`. Perlu seed/migrasi data atau set nullable dulu.
|
||||||
|
|
||||||
|
> **Strategi migrasi data:**
|
||||||
|
> - Buat `kategoriId` di `Umkm` menjadi nullable sementara (`String?`)
|
||||||
|
> - Jalankan migration
|
||||||
|
> - Seed `KategoriProdukUmkm` dengan data yang sama seperti `KategoriProduk`
|
||||||
|
> - Update data Umkm yang ada agar `kategoriId` menunjuk ke `KategoriProdukUmkm` yang sesuai
|
||||||
|
> - Set kembali `kategoriId` menjadi required (`String`)
|
||||||
|
|
||||||
|
### Fase 3 — API Handler: CRUD Kategori Produk UMKM
|
||||||
|
**File:** `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts`
|
||||||
|
|
||||||
|
Ganti semua `prisma.kategoriProduk` → `prisma.kategoriProdukUmkm`:
|
||||||
|
- `/find-many-all` → `prisma.kategoriProdukUmkm.findMany()`
|
||||||
|
- `/find-many` → `prisma.kategoriProdukUmkm.findMany()`
|
||||||
|
- `/create` → `prisma.kategoriProdukUmkm.create()`
|
||||||
|
- `PUT /:id` → `prisma.kategoriProdukUmkm.update()`
|
||||||
|
- `DELETE /del/:id` → `prisma.kategoriProdukUmkm.update()`
|
||||||
|
|
||||||
|
### Fase 4 — API Handler: Dashboard KPI
|
||||||
|
**File:** `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts`
|
||||||
|
|
||||||
|
- Bagian `groupBy kategoriId` untuk Umkm: ubah include/join agar ambil nama dari `KategoriProdukUmkm` bukan `KategoriProduk`
|
||||||
|
|
||||||
|
### Fase 5 — State Management
|
||||||
|
**File:** `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`
|
||||||
|
|
||||||
|
- Pastikan tipe data / form state kategori UMKM masih sesuai (field name tidak berubah, hanya model di backend)
|
||||||
|
|
||||||
|
### Fase 6 — Build & Verify
|
||||||
|
```bash
|
||||||
|
bun run tsc --noEmit # Type check
|
||||||
|
bun run build # Full build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Yang TIDAK Berubah
|
||||||
|
- `KategoriProduk` model tetap ada (masih digunakan `PasarDesa` dan `KategoriToPasar`)
|
||||||
|
- Relasi `PasarDesa.kategoriProdukId → KategoriProduk` tidak berubah
|
||||||
|
- Admin UI pages (kategori-produk/page.tsx, create, edit) — logika UI tidak berubah, hanya backend model berbeda
|
||||||
|
- API route path tetap sama (`/api/ekonomi/kategoriproduk/*`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risiko
|
||||||
|
- **Data migration:** Umkm yang ada punya `kategoriId` yang merujuk ke `KategoriProduk`. Perlu migrasi data atau data akan hilang relasi.
|
||||||
|
- **Solusi:** Buat `KategoriProdukUmkm` dengan data yang sama, lalu update `Umkm.kategoriId` via SQL migration script.
|
||||||
@@ -14,9 +14,9 @@ The edit pages for UMKM (Data UMKM and Produk) use an older UI pattern. The user
|
|||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
- [x] Analyze Berita edit page pattern
|
- [x] Analyze Berita edit page pattern
|
||||||
- [x] Refactor UMKM Produk edit page
|
- [x] Refactor UMKM Produk edit page (with interfaces)
|
||||||
- [x] Refactor Data UMKM edit page
|
- [x] Refactor Data UMKM edit page (with interfaces)
|
||||||
- [ ] Run build and fix any errors
|
- [x] Run build and fix any errors
|
||||||
- [ ] Update version in package.json
|
- [ ] Update version in package.json
|
||||||
- [ ] Commit and push to task branch
|
- [ ] Commit and push to task branch
|
||||||
- [ ] Merge to stg branch
|
- [ ] Merge to stg branch
|
||||||
|
|||||||
18
MIND/PLAN/task-fix-and-improve-umkm-module.md
Normal file
18
MIND/PLAN/task-fix-and-improve-umkm-module.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Task: Fix and Improve UMKM Module
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [x] Implement delete confirmation modal for UMKM sales.
|
||||||
|
- [x] Improve null handling in UMKM/Produk forms.
|
||||||
|
- [x] Fix field mapping in Produk edit page.
|
||||||
|
- [x] Refactor UMKM Dashboard KPI logic.
|
||||||
|
- [x] Add stock validation in sales API.
|
||||||
|
- [x] Fix WhatsApp link formatting.
|
||||||
|
- [x] Implement public product ordering system.
|
||||||
|
- [x] Update CLAUDE.md.
|
||||||
|
- [ ] Run build and fix errors.
|
||||||
|
- [ ] Update version in package.json.
|
||||||
|
- [ ] Commit changes.
|
||||||
|
|
||||||
|
## Progress Notes
|
||||||
|
- Changes have been implemented in the editor.
|
||||||
|
- Proceeding to build and commit.
|
||||||
12
MIND/PLAN/task-migrate-kategori-produk-umkm.md
Normal file
12
MIND/PLAN/task-migrate-kategori-produk-umkm.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Task: Migrasi KategoriProduk → KategoriProdukUmkm
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
- [x] Phase 1: Schema Update (`prisma/schema.prisma`) <!-- id: 0 -->
|
||||||
|
- [x] Phase 2: Data Migration (Manual SQL/Script) <!-- id: 1 -->
|
||||||
|
- [x] Phase 3: Update API CRUD UMKM Kategori <!-- id: 2 -->
|
||||||
|
- [x] Phase 4: Update KPI Dashboard UMKM <!-- id: 3 -->
|
||||||
|
- [x] Phase 5: Verification & Build <!-- id: 4 -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `KategoriProduk` tetap dipertahankan untuk `PasarDesa`.
|
||||||
|
- `KategoriProdukUmkm` akan digunakan secara eksklusif oleh `Umkm`.
|
||||||
@@ -4,9 +4,9 @@ Refactor Data UMKM and Produk edit pages to match the Berita edit page UI patter
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
1. [x] Analyze `berita/list-berita/[id]/edit/page.tsx` for the desired pattern.
|
1. [x] Analyze `berita/list-berita/[id]/edit/page.tsx` for the desired pattern.
|
||||||
2. [x] Implement the pattern in `ekonomi/umkm/produk/[id]/edit/page.tsx`.
|
2. [x] Implement the pattern in `ekonomi/umkm/produk/[id]/edit/page.tsx` (using interfaces).
|
||||||
3. [x] Implement the pattern in `ekonomi/umkm/data-umkm/[id]/edit/page.tsx`.
|
3. [x] Implement the pattern in `ekonomi/umkm/data-umkm/[id]/edit/page.tsx` (using interfaces).
|
||||||
4. [ ] Run `bun run build` to verify.
|
4. [x] Run `bun run build` to verify.
|
||||||
5. [ ] Update `package.json` version.
|
5. [x] Update `package.json` version.
|
||||||
6. [ ] Commit with message: "feat(admin): refactor UMKM edit pages to match berita pattern".
|
6. [x] Commit with message: "feat(admin): refactor UMKM edit pages to match berita pattern with interfaces".
|
||||||
7. [ ] Create summary in `MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md`.
|
7. [ ] Create summary in `MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md`.
|
||||||
|
|||||||
15
MIND/SUMMARY/fix-and-improve-umkm-module-summary.md
Normal file
15
MIND/SUMMARY/fix-and-improve-umkm-module-summary.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Summary: Fix and Improve UMKM Module
|
||||||
|
|
||||||
|
Successfully implemented various improvements and fixes for the UMKM module.
|
||||||
|
|
||||||
|
## Key Changes
|
||||||
|
- **Sales Management**: Added delete confirmation modal for sales history in the admin panel.
|
||||||
|
- **Data Integrity**: Improved null handling and field mapping in UMKM and Produk forms. Added stock validation when recording new sales.
|
||||||
|
- **Analytics**: Refactored dashboard KPI logic to better reflect top categories based on actual sales.
|
||||||
|
- **Public Features**: Implemented a "Pesan Sekarang" (Order Now) feature on the product detail page, which calculates totals and redirects to WhatsApp with a pre-filled message.
|
||||||
|
- **Bug Fixes**: Standardized WhatsApp link formatting across the site to use the `62` prefix correctly.
|
||||||
|
- **Documentation**: Translated and streamlined `CLAUDE.md` for better clarity.
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
- Build successful.
|
||||||
|
- Manual check of ordering and delete flows completed.
|
||||||
26
MIND/SUMMARY/migrate-kategori-produk-umkm-summary.md
Normal file
26
MIND/SUMMARY/migrate-kategori-produk-umkm-summary.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Summary: Migrasi KategoriProduk → KategoriProdukUmkm
|
||||||
|
|
||||||
|
## Perubahan yang Dilakukan
|
||||||
|
1. **Schema Prisma**:
|
||||||
|
- Memisahkan model kategori untuk `Umkm` dan `PasarDesa`.
|
||||||
|
- `Umkm` sekarang menggunakan `KategoriProdukUmkm`.
|
||||||
|
- `PasarDesa` tetap menggunakan `KategoriProduk`.
|
||||||
|
- Menghapus relasi `Umkm` dari model `KategoriProduk`.
|
||||||
|
2. **Database**:
|
||||||
|
- Menjalankan `prisma db push` untuk memperbarui tabel di PostgreSQL.
|
||||||
|
- Menyiapkan dan menguji script migrasi data (tabel saat ini kosong, namun script sudah diverifikasi).
|
||||||
|
3. **Backend API**:
|
||||||
|
- Mengubah `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts` agar menggunakan `prisma.kategoriProdukUmkm`.
|
||||||
|
- Memperbarui logic KPI dashboard di `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts` untuk menggunakan model kategori yang tepat berdasarkan konteks (UMKM vs Penjualan).
|
||||||
|
4. **Validasi**:
|
||||||
|
- Berhasil menjalankan `bun run build` tanpa error TypeScript baru.
|
||||||
|
|
||||||
|
## Dampak
|
||||||
|
- Admin UMKM sekarang memiliki manajemen kategori yang terisolasi dari PasarDesa.
|
||||||
|
- Tidak ada perubahan pada UI karena path API dan struktur data tetap sama.
|
||||||
|
- Kompabilitas data tetap terjaga karena relasi menggunakan ID yang sama.
|
||||||
|
|
||||||
|
## File Terkait
|
||||||
|
- `prisma/schema.prisma`
|
||||||
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts`
|
||||||
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts`
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
# Summary - Refactor UMKM Edit Pages Pattern
|
# Summary - Refactor UMKM Edit Pages Pattern
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
1. **UMKM Produk Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx` to match the "Berita" edit page pattern. Added Reset ("Batal") functionality, standardized header, paper, and dropzone styling, and used `EditEditor`.
|
1. **UMKM Produk Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx` to match the "Berita" edit page pattern. Added explicit `ProdukData` and `ProdukForm` interfaces. Added Reset ("Batal") functionality, standardized header, paper, and dropzone styling, and used `EditEditor`.
|
||||||
2. **Data UMKM Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx` with the same pattern and improvements.
|
2. **Data UMKM Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx` with the same pattern and interfaces (`UmkmData`, `UmkmForm`).
|
||||||
3. **UI Consistency**: Standardized colors and component usage across UMKM edit pages.
|
3. **Type Safety**: Improved type safety by using explicit interfaces for data fetching and form state management.
|
||||||
4. **UX Improvement**: Added a "Batal" button that resets the form to its original data state.
|
4. **UI Consistency**: Standardized colors and component usage across UMKM edit pages.
|
||||||
5. **Build Verification**: Confirmed that the project builds successfully with `bun run build`.
|
5. **UX Improvement**: Added a "Batal" button that resets the form to its original data state.
|
||||||
|
6. **Build Verification**: Confirmed that the project builds successfully with `bun run build`.
|
||||||
|
|
||||||
## Verification Results
|
## Verification Results
|
||||||
- `bun run build`: Success.
|
- `bun run build`: Success.
|
||||||
|
|||||||
57
bun.lock
57
bun.lock
@@ -71,6 +71,7 @@
|
|||||||
"list": "^2.0.19",
|
"list": "^2.0.19",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
|
"minio": "^8.0.7",
|
||||||
"motion": "^12.4.1",
|
"motion": "^12.4.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
@@ -401,6 +402,8 @@
|
|||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg=="],
|
||||||
|
|
||||||
|
"@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
@@ -981,6 +984,8 @@
|
|||||||
|
|
||||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
"async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
|
"async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
|
||||||
@@ -1007,15 +1012,19 @@
|
|||||||
|
|
||||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||||
|
|
||||||
|
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
|
"brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
@@ -1147,6 +1156,8 @@
|
|||||||
|
|
||||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||||
|
|
||||||
|
"decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||||
@@ -1297,6 +1308,10 @@
|
|||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
|
"fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.7.2", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.5", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
@@ -1315,6 +1330,8 @@
|
|||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="],
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||||
@@ -1427,7 +1444,7 @@
|
|||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
|
"iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
|
||||||
|
|
||||||
@@ -1633,6 +1650,8 @@
|
|||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"minio": ["minio@8.0.7", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^5.3.4", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ=="],
|
||||||
|
|
||||||
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
||||||
@@ -1729,6 +1748,8 @@
|
|||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
|
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
@@ -1831,6 +1852,8 @@
|
|||||||
|
|
||||||
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||||
|
|
||||||
|
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"ramda": ["ramda@0.27.2", "", {}, "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="],
|
"ramda": ["ramda@0.27.2", "", {}, "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="],
|
||||||
@@ -1875,6 +1898,8 @@
|
|||||||
|
|
||||||
"react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="],
|
"react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
||||||
@@ -1925,6 +1950,8 @@
|
|||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||||
|
|
||||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
@@ -1967,6 +1994,8 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
|
||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
@@ -1977,8 +2006,14 @@
|
|||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
|
"stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
|
||||||
|
|
||||||
|
"stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="],
|
||||||
|
|
||||||
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
|
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
|
||||||
|
|
||||||
|
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
@@ -1993,6 +2028,8 @@
|
|||||||
|
|
||||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
@@ -2001,6 +2038,8 @@
|
|||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
|
||||||
|
|
||||||
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
||||||
|
|
||||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
@@ -2023,6 +2062,8 @@
|
|||||||
|
|
||||||
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
|
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
|
||||||
|
|
||||||
|
"through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
@@ -2159,6 +2200,10 @@
|
|||||||
|
|
||||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||||
|
|
||||||
|
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||||
|
|
||||||
|
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
@@ -2253,6 +2298,8 @@
|
|||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
|
"minio/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"msgpackr-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
"msgpackr-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
@@ -2267,12 +2314,16 @@
|
|||||||
|
|
||||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
"string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||||
|
|
||||||
|
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"@parcel/packager-js/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
"@parcel/packager-js/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||||
@@ -2289,6 +2340,8 @@
|
|||||||
|
|
||||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"minio/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ set -e
|
|||||||
|
|
||||||
echo "🔄 Running database migrations..."
|
echo "🔄 Running database migrations..."
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
|
# Resolve any previously failed migrations before re-applying
|
||||||
|
bunx prisma migrate resolve --rolled-back 20260423072135_add_stok_to_pasar_desa 2>/dev/null || true
|
||||||
|
|
||||||
bunx prisma migrate deploy || {
|
bunx prisma migrate deploy || {
|
||||||
echo "❌ Migration failed!"
|
echo "❌ Migration failed!"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.22",
|
"version": "0.1.35",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
const kategoriUmkmData = [
|
||||||
|
{ id: "4b95bge6-012e-5ged-9552-4d8g65d44959", nama: "Makanan" },
|
||||||
|
{ id: "5c06chf7-123f-6hfe-0663-5e9h76e55060", nama: "Minuman" },
|
||||||
|
{ id: "5c06chf7-123f-7igd-0663-5e9h76e55060", nama: "Sembako" },
|
||||||
|
{ id: "5c06chf7-123f-8jhe-0663-5e9h76e55060", nama: "Sayur Mayur" },
|
||||||
|
{ id: "5c06chf7-123f-9kif-0663-5e9h76e55060", nama: "Protein Hewani" },
|
||||||
|
];
|
||||||
|
|
||||||
export const umkmData = [
|
export const umkmData = [
|
||||||
{
|
{
|
||||||
id: "umkm-1",
|
id: "umkm-1",
|
||||||
@@ -40,6 +48,15 @@ export const umkmData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export async function seedUmkm() {
|
export async function seedUmkm() {
|
||||||
|
console.log("🔄 Seeding Kategori Produk UMKM...");
|
||||||
|
for (const k of kategoriUmkmData) {
|
||||||
|
await prisma.kategoriProdukUmkm.upsert({
|
||||||
|
where: { id: k.id },
|
||||||
|
update: { nama: k.nama },
|
||||||
|
create: { id: k.id, nama: k.nama },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log("🔄 Seeding UMKM...");
|
console.log("🔄 Seeding UMKM...");
|
||||||
for (const u of umkmData) {
|
for (const u of umkmData) {
|
||||||
await prisma.umkm.upsert({
|
await prisma.umkm.upsert({
|
||||||
|
|||||||
@@ -5,33 +5,85 @@
|
|||||||
- Added the required column `umkmId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
|
- Added the required column `umkmId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
-- DropForeignKey
|
-- DropForeignKey (idempotent)
|
||||||
ALTER TABLE "PenjualanProduk" DROP CONSTRAINT "PenjualanProduk_produkId_fkey";
|
ALTER TABLE "PenjualanProduk" DROP CONSTRAINT IF EXISTS "PenjualanProduk_produkId_fkey";
|
||||||
|
ALTER TABLE "ProdukUmkm" DROP CONSTRAINT IF EXISTS "ProdukUmkm_imageId_fkey";
|
||||||
|
ALTER TABLE "ProdukUmkm" DROP CONSTRAINT IF EXISTS "ProdukUmkm_umkmId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- AlterTable KategoriProduk (idempotent via DO block)
|
||||||
ALTER TABLE "ProdukUmkm" DROP CONSTRAINT "ProdukUmkm_imageId_fkey";
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE "KategoriProduk" ALTER COLUMN "deletedAt" DROP NOT NULL;
|
||||||
|
EXCEPTION WHEN others THEN NULL;
|
||||||
|
END;
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE "KategoriProduk" ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
EXCEPTION WHEN others THEN NULL;
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- DropForeignKey
|
-- AlterTable PasarDesa: add columns if not exists, handle NOT NULL safely
|
||||||
ALTER TABLE "ProdukUmkm" DROP CONSTRAINT "ProdukUmkm_umkmId_fkey";
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'PasarDesa' AND column_name = 'stok'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "PasarDesa" ADD COLUMN "stok" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
END IF;
|
||||||
|
|
||||||
-- AlterTable
|
IF NOT EXISTS (
|
||||||
ALTER TABLE "KategoriProduk" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
SELECT 1 FROM information_schema.columns
|
||||||
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
WHERE table_name = 'PasarDesa' AND column_name = 'umkmId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "PasarDesa" ADD COLUMN "umkmId" TEXT;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- Set default value for existing rows before making NOT NULL
|
||||||
ALTER TABLE "PasarDesa" ADD COLUMN "stok" INTEGER NOT NULL DEFAULT 0,
|
UPDATE "PasarDesa" SET "umkmId" = '' WHERE "umkmId" IS NULL;
|
||||||
ADD COLUMN "umkmId" TEXT NOT NULL,
|
ALTER TABLE "PasarDesa" ALTER COLUMN "umkmId" SET NOT NULL;
|
||||||
ALTER COLUMN "rating" SET DEFAULT 0,
|
|
||||||
ALTER COLUMN "alamatUsaha" DROP NOT NULL,
|
|
||||||
ALTER COLUMN "deletedAt" DROP NOT NULL,
|
|
||||||
ALTER COLUMN "deletedAt" DROP DEFAULT,
|
|
||||||
ALTER COLUMN "kontak" DROP NOT NULL;
|
|
||||||
|
|
||||||
-- DropTable
|
-- Remaining PasarDesa alterations (idempotent)
|
||||||
DROP TABLE "ProdukUmkm";
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
BEGIN ALTER TABLE "PasarDesa" ALTER COLUMN "rating" SET DEFAULT 0; EXCEPTION WHEN others THEN NULL; END;
|
||||||
|
BEGIN ALTER TABLE "PasarDesa" ALTER COLUMN "alamatUsaha" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END;
|
||||||
|
BEGIN ALTER TABLE "PasarDesa" ALTER COLUMN "deletedAt" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END;
|
||||||
|
BEGIN ALTER TABLE "PasarDesa" ALTER COLUMN "deletedAt" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END;
|
||||||
|
BEGIN ALTER TABLE "PasarDesa" ALTER COLUMN "kontak" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- DropTable (idempotent)
|
||||||
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_umkmId_fkey" FOREIGN KEY ("umkmId") REFERENCES "Umkm"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DROP TABLE IF EXISTS "ProdukUmkm";
|
||||||
|
|
||||||
-- AddForeignKey
|
-- Clean up rows with invalid umkmId before adding FK constraint
|
||||||
ALTER TABLE "PenjualanProduk" ADD CONSTRAINT "PenjualanProduk_produkId_fkey" FOREIGN KEY ("produkId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
-- Must delete child tables first to avoid FK violations
|
||||||
|
DELETE FROM "KategoriToPasar" WHERE "pasarDesaId" IN (
|
||||||
|
SELECT id FROM "PasarDesa" WHERE "umkmId" = '' OR "umkmId" NOT IN (SELECT id FROM "Umkm")
|
||||||
|
);
|
||||||
|
DELETE FROM "PenjualanProduk" WHERE "produkId" IN (
|
||||||
|
SELECT id FROM "PasarDesa" WHERE "umkmId" = '' OR "umkmId" NOT IN (SELECT id FROM "Umkm")
|
||||||
|
);
|
||||||
|
DELETE FROM "PasarDesa" WHERE "umkmId" = '' OR "umkmId" NOT IN (SELECT id FROM "Umkm");
|
||||||
|
|
||||||
|
-- AddForeignKey (idempotent)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'PasarDesa_umkmId_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_umkmId_fkey"
|
||||||
|
FOREIGN KEY ("umkmId") REFERENCES "Umkm"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'PenjualanProduk_produkId_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "PenjualanProduk" ADD CONSTRAINT "PenjualanProduk_produkId_fkey"
|
||||||
|
FOREIGN KEY ("produkId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Create KategoriProdukUmkm table if it doesn't exist
|
||||||
|
-- (renames existing kategori_produk_umkm if present, otherwise creates fresh)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name = 'kategori_produk_umkm'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "kategori_produk_umkm" RENAME TO "KategoriProdukUmkm";
|
||||||
|
ELSIF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name = 'KategoriProdukUmkm'
|
||||||
|
) THEN
|
||||||
|
CREATE TABLE "KategoriProdukUmkm" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"nama" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
CONSTRAINT "KategoriProdukUmkm_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Seed KategoriProdukUmkm: copy from KategoriProduk where referenced by Umkm
|
||||||
|
-- This ensures existing Umkm.kategoriId values exist in the new table
|
||||||
|
INSERT INTO "KategoriProdukUmkm" ("id", "nama", "createdAt", "updatedAt", "deletedAt", "isActive")
|
||||||
|
SELECT DISTINCT kp.id, kp.nama, kp."createdAt", kp."updatedAt", kp."deletedAt", kp."isActive"
|
||||||
|
FROM "KategoriProduk" kp
|
||||||
|
WHERE kp.id IN (SELECT DISTINCT "kategoriId" FROM "Umkm")
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM "KategoriProdukUmkm" WHERE id = kp.id);
|
||||||
|
|
||||||
|
-- Update Umkm FK: drop old FK pointing to KategoriProduk, add new one to KategoriProdukUmkm
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
fk_target TEXT;
|
||||||
|
BEGIN
|
||||||
|
SELECT ccu.table_name INTO fk_target
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu
|
||||||
|
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
||||||
|
JOIN information_schema.constraint_column_usage ccu
|
||||||
|
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||||
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
AND tc.table_schema = 'public'
|
||||||
|
AND tc.table_name = 'Umkm'
|
||||||
|
AND kcu.column_name = 'kategoriId'
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF fk_target = 'KategoriProduk' THEN
|
||||||
|
ALTER TABLE "Umkm" DROP CONSTRAINT "Umkm_kategoriId_fkey";
|
||||||
|
ALTER TABLE "Umkm" ADD CONSTRAINT "Umkm_kategoriId_fkey"
|
||||||
|
FOREIGN KEY ("kategoriId") REFERENCES "KategoriProdukUmkm"("id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -1459,7 +1459,6 @@ model KategoriProduk {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
KategoriToPasar KategoriToPasar[]
|
KategoriToPasar KategoriToPasar[]
|
||||||
PasarDesa PasarDesa[]
|
PasarDesa PasarDesa[]
|
||||||
Umkm Umkm[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriToPasar {
|
model KategoriToPasar {
|
||||||
@@ -2424,23 +2423,34 @@ model MusikDesa {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Umkm {
|
model Umkm {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
nama String
|
nama String
|
||||||
pemilik String
|
pemilik String
|
||||||
deskripsi String?
|
deskripsi String?
|
||||||
alamat String?
|
alamat String?
|
||||||
kontak String?
|
kontak String?
|
||||||
image FileStorage? @relation("UmkmImage", fields: [imageId], references: [id])
|
image FileStorage? @relation("UmkmImage", fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
kategori KategoriProduk @relation(fields: [kategoriId], references: [id])
|
kategori KategoriProdukUmkm @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String
|
kategoriId String
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
produk PasarDesa[]
|
produk PasarDesa[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model KategoriProdukUmkm {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
nama String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
Umkm Umkm[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
model PenjualanProduk {
|
model PenjualanProduk {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
produk PasarDesa @relation(fields: [produkId], references: [id])
|
produk PasarDesa @relation(fields: [produkId], references: [id])
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ const umkmFormSchema = z.object({
|
|||||||
nama: z.string().min(1, "Nama minimal 1 karakter"),
|
nama: z.string().min(1, "Nama minimal 1 karakter"),
|
||||||
pemilik: z.string().min(1, "Nama pemilik wajib diisi"),
|
pemilik: z.string().min(1, "Nama pemilik wajib diisi"),
|
||||||
kategoriId: z.string().min(1, "Kategori wajib dipilih"),
|
kategoriId: z.string().min(1, "Kategori wajib dipilih"),
|
||||||
deskripsi: z.string().optional(),
|
deskripsi: z.string().optional().nullable(),
|
||||||
alamat: z.string().optional(),
|
alamat: z.string().optional().nullable(),
|
||||||
kontak: z.string().optional(),
|
kontak: z.string().optional().nullable(),
|
||||||
imageId: z.string().optional(),
|
imageId: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultUmkmForm = {
|
const defaultUmkmForm = {
|
||||||
@@ -21,7 +21,7 @@ const defaultUmkmForm = {
|
|||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
alamat: "",
|
alamat: "",
|
||||||
kontak: "",
|
kontak: "",
|
||||||
imageId: "",
|
imageId: null as string | null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ const produkFormSchema = z.object({
|
|||||||
harga: z.number().min(0, "Harga tidak boleh negatif"),
|
harga: z.number().min(0, "Harga tidak boleh negatif"),
|
||||||
stok: z.number().min(0, "Stok tidak boleh negatif"),
|
stok: z.number().min(0, "Stok tidak boleh negatif"),
|
||||||
umkmId: z.string().min(1, "UMKM wajib dipilih"),
|
umkmId: z.string().min(1, "UMKM wajib dipilih"),
|
||||||
deskripsi: z.string().optional(),
|
deskripsi: z.string().optional().nullable(),
|
||||||
imageId: z.string().optional(),
|
imageId: z.string().optional().nullable(),
|
||||||
kategoriId: z.string().min(1, "Kategori wajib dipilih"), // PasarDesa needs category
|
kategoriId: z.string().min(1, "Kategori wajib dipilih"), // PasarDesa needs category
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ const defaultProdukForm = {
|
|||||||
stok: 0,
|
stok: 0,
|
||||||
umkmId: "",
|
umkmId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: null as string | null,
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
@@ -63,6 +63,15 @@ const defaultPenjualanForm = {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kategori Produk Form Validation
|
||||||
|
const kategoriProdukFormSchema = z.object({
|
||||||
|
nama: z.string().min(1, "Nama kategori wajib diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultKategoriProdukForm = {
|
||||||
|
nama: "",
|
||||||
|
};
|
||||||
|
|
||||||
export const umkmState = proxy({
|
export const umkmState = proxy({
|
||||||
// UMKM Module
|
// UMKM Module
|
||||||
umkm: {
|
umkm: {
|
||||||
@@ -101,7 +110,7 @@ export const umkmState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async submit() {
|
async submit() {
|
||||||
const cek = umkmFormSchema.safeParse(this.form);
|
const cek = umkmFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) return toast.error("Cek kembali form anda");
|
if (!cek.success) { toast.error("Cek kembali form anda"); return false; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ekonomi/umkm/create", {
|
const res = await fetch("/api/ekonomi/umkm/create", {
|
||||||
@@ -129,7 +138,7 @@ export const umkmState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string) {
|
async submit(id: string) {
|
||||||
const cek = umkmFormSchema.safeParse(this.form);
|
const cek = umkmFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) return toast.error("Cek kembali form anda");
|
if (!cek.success) { toast.error("Cek kembali form anda"); return false; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/ekonomi/umkm/${id}`, {
|
const res = await fetch(`/api/ekonomi/umkm/${id}`, {
|
||||||
@@ -237,7 +246,7 @@ export const umkmState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async submit() {
|
async submit() {
|
||||||
const cek = produkFormSchema.safeParse(this.form);
|
const cek = produkFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) return toast.error("Cek kembali form anda");
|
if (!cek.success) { toast.error("Cek kembali form anda"); return false; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ekonomi/umkm/produk/create", {
|
const res = await fetch("/api/ekonomi/umkm/produk/create", {
|
||||||
@@ -260,7 +269,7 @@ export const umkmState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string) {
|
async submit(id: string) {
|
||||||
const cek = produkFormSchema.safeParse(this.form);
|
const cek = produkFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) return toast.error("Cek kembali form anda");
|
if (!cek.success) { toast.error("Cek kembali form anda"); return false; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/ekonomi/umkm/produk/${id}`, {
|
const res = await fetch(`/api/ekonomi/umkm/produk/${id}`, {
|
||||||
@@ -323,7 +332,7 @@ export const umkmState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async submit() {
|
async submit() {
|
||||||
const cek = penjualanFormSchema.safeParse(this.form);
|
const cek = penjualanFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) return toast.error("Cek kembali form anda");
|
if (!cek.success) { toast.error("Cek kembali form anda"); return false; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ekonomi/umkm/penjualan/create", {
|
const res = await fetch("/api/ekonomi/umkm/penjualan/create", {
|
||||||
@@ -340,11 +349,59 @@ export const umkmState = proxy({
|
|||||||
} catch (e) { toast.error("Gagal mencatat penjualan"); } finally { this.loading = false; }
|
} catch (e) { toast.error("Gagal mencatat penjualan"); } finally { this.loading = false; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
del: {
|
||||||
|
loading: false,
|
||||||
|
async submit(id: string) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ekonomi/umkm/penjualan/del/${id}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Histori penjualan berhasil dihapus");
|
||||||
|
umkmState.penjualan.findMany.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toast.error(result.message);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Gagal menghapus histori penjualan");
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Kategori Produk (Share with Pasar Desa)
|
// Kategori Produk (Share with Pasar Desa)
|
||||||
kategoriProduk: {
|
kategoriProduk: {
|
||||||
|
findMany: {
|
||||||
|
data: [] as any[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
async load(page = 1, limit = 10, search = "") {
|
||||||
|
this.loading = true;
|
||||||
|
this.page = page;
|
||||||
|
this.search = search;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
limit: limit.toString(),
|
||||||
|
search
|
||||||
|
});
|
||||||
|
const res = await fetch(`/api/ekonomi/kategoriproduk/find-many?${params}`);
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
this.totalPages = result.totalPages;
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); } finally { this.loading = false; }
|
||||||
|
}
|
||||||
|
},
|
||||||
findManyAll: {
|
findManyAll: {
|
||||||
data: [] as any[],
|
data: [] as any[],
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -358,6 +415,75 @@ export const umkmState = proxy({
|
|||||||
}
|
}
|
||||||
} catch (e) { console.error(e); } finally { this.loading = false; }
|
} catch (e) { console.error(e); } finally { this.loading = false; }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
form: { ...defaultKategoriProdukForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = kategoriProdukFormSchema.safeParse(this.form);
|
||||||
|
if (!cek.success) { toast.error("Nama kategori wajib diisi"); return false; }
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/ekonomi/kategoriproduk/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Kategori berhasil dibuat");
|
||||||
|
umkmState.kategoriProduk.findMany.load();
|
||||||
|
umkmState.kategoriProduk.findManyAll.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toast.error(result.message || "Gagal membuat kategori");
|
||||||
|
} catch (e) { toast.error("Gagal membuat kategori"); } finally { this.loading = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
form: { ...defaultKategoriProdukForm },
|
||||||
|
loading: false,
|
||||||
|
async submit(id: string) {
|
||||||
|
const cek = kategoriProdukFormSchema.safeParse(this.form);
|
||||||
|
if (!cek.success) { toast.error("Nama kategori wajib diisi"); return false; }
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Kategori berhasil diperbarui");
|
||||||
|
umkmState.kategoriProduk.findMany.load();
|
||||||
|
umkmState.kategoriProduk.findManyAll.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toast.error(result.message || "Gagal memperbarui kategori");
|
||||||
|
} catch (e) { toast.error("Gagal memperbarui kategori"); } finally { this.loading = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
del: {
|
||||||
|
loading: false,
|
||||||
|
async submit(id: string) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Kategori berhasil dihapus");
|
||||||
|
umkmState.kategoriProduk.findMany.load();
|
||||||
|
umkmState.kategoriProduk.findManyAll.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { toast.error("Gagal menghapus kategori"); } finally { this.loading = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
TabsTab,
|
TabsTab,
|
||||||
Title
|
Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDashboard, IconBuildingStore, IconPackage, IconShoppingCart } from '@tabler/icons-react';
|
import { IconDashboard, IconBuildingStore, IconPackage, IconShoppingCart, IconTag } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -44,6 +44,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
href: "/admin/ekonomi/umkm/penjualan",
|
href: "/admin/ekonomi/umkm/penjualan",
|
||||||
icon: <IconShoppingCart size={18} stroke={1.8} />
|
icon: <IconShoppingCart size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori Produk",
|
||||||
|
value: "kategori-produk",
|
||||||
|
href: "/admin/ekonomi/umkm/kategori-produk",
|
||||||
|
icon: <IconTag size={18} stroke={1.8} />
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = tabs.find((tab) => pathname.startsWith(tab.href));
|
const currentTab = tabs.find((tab) => pathname.startsWith(tab.href));
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function UmkmDashboard() {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||||
<Card withBorder radius="md" p="lg" shadow="sm">
|
<Card h={"100%"} withBorder radius="md" p="lg" shadow="sm">
|
||||||
<Title order={4} mb="md">Top 3 Produk</Title>
|
<Title order={4} mb="md">Top 3 Produk</Title>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{topProduk.map((item, i) => (
|
{topProduk.map((item, i) => (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
@@ -32,35 +31,56 @@ import { toast } from "react-toastify";
|
|||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
import umkmState from "../../../../../_state/ekonomi/umkm/umkm";
|
import umkmState from "../../../../../_state/ekonomi/umkm/umkm";
|
||||||
|
|
||||||
|
interface UmkmData {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
pemilik: string;
|
||||||
|
kategoriId: string | null;
|
||||||
|
deskripsi: string | null;
|
||||||
|
alamat: string | null;
|
||||||
|
kontak: string | null;
|
||||||
|
imageId: string | null;
|
||||||
|
image?: { link: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UmkmForm {
|
||||||
|
nama: string;
|
||||||
|
pemilik: string;
|
||||||
|
kategoriId: string;
|
||||||
|
deskripsi: string;
|
||||||
|
alamat: string;
|
||||||
|
kontak: string;
|
||||||
|
imageId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function EditDataUmkm() {
|
function EditDataUmkm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
const state = useProxy(umkmState.umkm);
|
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<UmkmForm>({
|
||||||
nama: "",
|
nama: "",
|
||||||
pemilik: "",
|
pemilik: "",
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
alamat: "",
|
alamat: "",
|
||||||
kontak: "",
|
kontak: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState<UmkmForm & { imageUrl: string }>({
|
||||||
nama: "",
|
nama: "",
|
||||||
pemilik: "",
|
pemilik: "",
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
alamat: "",
|
alamat: "",
|
||||||
kontak: "",
|
kontak: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
imageUrl: ""
|
imageUrl: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,9 +99,9 @@ function EditDataUmkm() {
|
|||||||
umkmState.umkm.findUnique.load(id)
|
umkmState.umkm.findUnique.load(id)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const data = umkmState.umkm.findUnique.data;
|
const data = umkmState.umkm.findUnique.data as UmkmData | null;
|
||||||
if (data) {
|
if (data) {
|
||||||
const initialForm = {
|
const initialForm: UmkmForm = {
|
||||||
nama: data.nama || "",
|
nama: data.nama || "",
|
||||||
pemilik: data.pemilik || "",
|
pemilik: data.pemilik || "",
|
||||||
kategoriId: data.kategoriId || "",
|
kategoriId: data.kategoriId || "",
|
||||||
@@ -94,11 +114,11 @@ function EditDataUmkm() {
|
|||||||
setFormData(initialForm);
|
setFormData(initialForm);
|
||||||
setOriginalData({
|
setOriginalData({
|
||||||
...initialForm,
|
...initialForm,
|
||||||
imageUrl: data.image?.url || ""
|
imageUrl: data.image?.link || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.image?.url) {
|
if (data.image?.link) {
|
||||||
setPreviewImage(data.image.url);
|
setPreviewImage(data.image.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsInitialLoading(false);
|
setIsInitialLoading(false);
|
||||||
@@ -106,7 +126,7 @@ function EditDataUmkm() {
|
|||||||
init();
|
init();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const handleChange = (field: string, value: string) => {
|
const handleChange = (field: keyof UmkmForm, value: string) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,6 +280,7 @@ function EditDataUmkm() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
setFormData(prev => ({ ...prev, imageId: null }));
|
||||||
}}
|
}}
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||||
>
|
>
|
||||||
@@ -291,7 +312,7 @@ function EditDataUmkm() {
|
|||||||
label="Kategori Bisnis"
|
label="Kategori Bisnis"
|
||||||
placeholder="Pilih kategori"
|
placeholder="Pilih kategori"
|
||||||
required
|
required
|
||||||
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
|
data={umkmState.kategoriProduk.findManyAll.data?.map((v: any) => ({
|
||||||
value: v.id, label: v.nama
|
value: v.id, label: v.nama
|
||||||
})) || []}
|
})) || []}
|
||||||
value={formData.kategoriId}
|
value={formData.kategoriId}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function CreateDataUmkm() {
|
|||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
alamat: "",
|
alamat: "",
|
||||||
kontak: "",
|
kontak: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
@@ -53,7 +53,7 @@ export default function CreateDataUmkm() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// 1. Upload image first if exists
|
// 1. Upload image first if exists
|
||||||
let uploadedImageId = "";
|
let uploadedImageId = null;
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Center,
|
||||||
|
Loader
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
export default function EditKategoriProduk() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const id = params.id as string;
|
||||||
|
const state = useProxy(umkmState.kategoriProduk.findMany);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [nama, setNama] = useState("");
|
||||||
|
const [originalNama, setOriginalNama] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// Find the item from the existing list or load it if not available
|
||||||
|
if (state.data.length === 0) {
|
||||||
|
await state.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = state.data.find((v: any) => v.id === id);
|
||||||
|
if (item) {
|
||||||
|
setNama(item.nama);
|
||||||
|
setOriginalNama(item.nama);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
init();
|
||||||
|
}, [id, state]);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setNama(originalNama);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
umkmState.kategoriProduk.update.form.nama = nama;
|
||||||
|
const success = await umkmState.kategoriProduk.update.submit(id);
|
||||||
|
if (success) {
|
||||||
|
router.push('/admin/ekonomi/umkm/kategori-produk');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Center h={400}>
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Group mb="lg">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={20} />}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Title order={3}>Edit Kategori Produk</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="xl" radius="md" shadow="sm" maw={600}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<TextInput
|
||||||
|
label="Nama Kategori"
|
||||||
|
placeholder="Contoh: Makanan, Minuman, Kerajinan"
|
||||||
|
required
|
||||||
|
value={nama}
|
||||||
|
onChange={(e) => setNama(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="outline" color="gray" onClick={handleResetForm}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onClick={handleUpdate}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={!nama.trim()}
|
||||||
|
>
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
export default function CreateKategoriProduk() {
|
||||||
|
const router = useRouter();
|
||||||
|
const state = useProxy(umkmState.kategoriProduk.create);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
state.form = {
|
||||||
|
nama: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const success = await umkmState.kategoriProduk.create.submit();
|
||||||
|
if (success) {
|
||||||
|
handleResetForm();
|
||||||
|
router.push('/admin/ekonomi/umkm/kategori-produk');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Group mb="lg">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={20} />}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Title order={3}>Tambah Kategori Produk Baru</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="xl" radius="md" shadow="sm" maw={600}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<TextInput
|
||||||
|
label="Nama Kategori"
|
||||||
|
placeholder="Contoh: Makanan, Minuman, Kerajinan"
|
||||||
|
required
|
||||||
|
value={state.form.nama}
|
||||||
|
onChange={(e) => (state.form.nama = e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="outline" color="gray" onClick={handleResetForm}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onClick={handleCreate}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Simpan Kategori
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx
Normal file
144
src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
'use client'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Title,
|
||||||
|
TextInput,
|
||||||
|
Badge
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
|
function KategoriProdukPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const state = useProxy(umkmState.kategoriProduk.findMany);
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.load(state.page, 10, debouncedSearch);
|
||||||
|
}, [state.page, debouncedSearch]);
|
||||||
|
|
||||||
|
const handleHapus = async () => {
|
||||||
|
if (selectedId) {
|
||||||
|
const success = await umkmState.kategoriProduk.del.submit(selectedId);
|
||||||
|
if (success) {
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={3}>Kategori Produk</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push('/admin/ekonomi/umkm/kategori-produk/create')}
|
||||||
|
>
|
||||||
|
Tambah Kategori
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari kategori..."
|
||||||
|
leftSection={<IconSearch size={18} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{state.loading ? (
|
||||||
|
<Skeleton height={400} />
|
||||||
|
) : (
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama Kategori</TableTh>
|
||||||
|
<TableTh>Status</TableTh>
|
||||||
|
<TableTh>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{state.data.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd fw={500}>{item.nama}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Badge color={item.isActive ? "green" : "red"}>
|
||||||
|
{item.isActive ? "Aktif" : "Nonaktif"}
|
||||||
|
</Badge>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="blue"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => router.push(`/admin/ekonomi/umkm/kategori-produk/${item.id}/edit`)}
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
total={state.totalPages}
|
||||||
|
value={state.page}
|
||||||
|
onChange={(p) => state.load(p, 10, debouncedSearch)}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah Anda yakin ingin menghapus kategori ini? Kategori yang dihapus tidak akan muncul di pilihan kategori produk baru."
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KategoriProdukPage;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -14,21 +15,40 @@ import {
|
|||||||
TableTh,
|
TableTh,
|
||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Title,
|
||||||
Title
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function PenjualanUmkm() {
|
function PenjualanUmkm() {
|
||||||
const state = useProxy(umkmState.penjualan.findMany);
|
const state = useProxy(umkmState.penjualan.findMany);
|
||||||
|
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.load(state.page, 10);
|
state.load(state.page, 10);
|
||||||
}, [state.page]);
|
}, [state.page]);
|
||||||
|
|
||||||
|
const handleHapus = async () => {
|
||||||
|
if (!selectedId) return;
|
||||||
|
|
||||||
|
const success = await umkmState.penjualan.del.submit(selectedId);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
|
||||||
|
// 🔥 reload data
|
||||||
|
state.load(state.page, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -54,16 +74,30 @@ function PenjualanUmkm() {
|
|||||||
<TableTh>Aksi</TableTh>
|
<TableTh>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
|
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{state.data.map((item) => (
|
{state.data.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{new Date(item.tanggal).toLocaleDateString('id-ID')}</TableTd>
|
<TableTd>
|
||||||
|
{new Date(item.tanggal).toLocaleDateString('id-ID')}
|
||||||
|
</TableTd>
|
||||||
<TableTd fw={500}>{item.produk?.nama}</TableTd>
|
<TableTd fw={500}>{item.produk?.nama}</TableTd>
|
||||||
<TableTd>{item.produk?.umkm?.nama}</TableTd>
|
<TableTd>{item.produk?.umkm?.nama}</TableTd>
|
||||||
<TableTd>{item.jumlah}</TableTd>
|
<TableTd>{item.jumlah}</TableTd>
|
||||||
<TableTd fw={600}>Rp {item.totalNilai.toLocaleString()}</TableTd>
|
<TableTd fw={600}>
|
||||||
|
Rp {item.totalNilai.toLocaleString()}
|
||||||
|
</TableTd>
|
||||||
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button variant="subtle" color="red" size="xs">
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IconTrash size={16} />
|
<IconTrash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -82,8 +116,16 @@ function PenjualanUmkm() {
|
|||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* 🔥 Modal Konfirmasi */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah Anda yakin ingin menghapus data penjualan ini?"
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PenjualanUmkm;
|
export default PenjualanUmkm;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
@@ -33,43 +32,59 @@ import { toast } from "react-toastify";
|
|||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
import umkmState from "../../../../../_state/ekonomi/umkm/umkm";
|
import umkmState from "../../../../../_state/ekonomi/umkm/umkm";
|
||||||
|
|
||||||
|
interface ProdukData {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
harga: number;
|
||||||
|
stok: number;
|
||||||
|
umkmId: string | null;
|
||||||
|
deskripsi: string | null;
|
||||||
|
imageId: string | null;
|
||||||
|
kategoriProdukId: string | null;
|
||||||
|
image?: { link: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProdukForm {
|
||||||
|
nama: string;
|
||||||
|
harga: number;
|
||||||
|
stok: number;
|
||||||
|
umkmId: string;
|
||||||
|
deskripsi: string;
|
||||||
|
imageId: string | null;
|
||||||
|
kategoriId: string;
|
||||||
|
}
|
||||||
|
|
||||||
function EditProdukUmkm() {
|
function EditProdukUmkm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
const state = useProxy(umkmState.produk);
|
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<ProdukForm>({
|
||||||
nama: "",
|
nama: "",
|
||||||
harga: 0,
|
harga: 0,
|
||||||
stok: 0,
|
stok: 0,
|
||||||
umkmId: "",
|
umkmId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState<ProdukForm & { imageUrl: string }>({
|
||||||
nama: "",
|
nama: "",
|
||||||
harga: 0,
|
harga: 0,
|
||||||
stok: 0,
|
stok: 0,
|
||||||
umkmId: "",
|
umkmId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
imageUrl: ""
|
imageUrl: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const isHtmlEmpty = (html: string) => {
|
|
||||||
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
|
||||||
return textContent === '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
return (
|
return (
|
||||||
formData.nama?.trim() !== '' &&
|
formData.nama?.trim() !== '' &&
|
||||||
@@ -88,26 +103,26 @@ function EditProdukUmkm() {
|
|||||||
umkmState.produk.findUnique.load(id)
|
umkmState.produk.findUnique.load(id)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const data = umkmState.produk.findUnique.data;
|
const data = umkmState.produk.findUnique.data as ProdukData | null;
|
||||||
if (data) {
|
if (data) {
|
||||||
const initialForm = {
|
const initialForm: ProdukForm = {
|
||||||
nama: data.nama || "",
|
nama: data.nama || "",
|
||||||
harga: data.harga || 0,
|
harga: data.harga || 0,
|
||||||
stok: data.stok || 0,
|
stok: data.stok || 0,
|
||||||
umkmId: data.umkmId || "",
|
umkmId: data.umkmId || "",
|
||||||
deskripsi: data.deskripsi || "",
|
deskripsi: data.deskripsi || "",
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || "",
|
||||||
kategoriId: data.kategoriId || "",
|
kategoriId: data.kategoriProdukId || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormData(initialForm);
|
setFormData(initialForm);
|
||||||
setOriginalData({
|
setOriginalData({
|
||||||
...initialForm,
|
...initialForm,
|
||||||
imageUrl: data.image?.url || ""
|
imageUrl: data.image?.link || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.image?.url) {
|
if (data.image?.link) {
|
||||||
setPreviewImage(data.image.url);
|
setPreviewImage(data.image.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsInitialLoading(false);
|
setIsInitialLoading(false);
|
||||||
@@ -115,7 +130,7 @@ function EditProdukUmkm() {
|
|||||||
init();
|
init();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const handleChange = (field: string, value: any) => {
|
const handleChange = (field: keyof ProdukForm, value: string | number) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,6 +284,7 @@ function EditProdukUmkm() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
setFormData(prev => ({ ...prev, imageId: null }));
|
||||||
}}
|
}}
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||||
>
|
>
|
||||||
@@ -284,7 +300,7 @@ function EditProdukUmkm() {
|
|||||||
placeholder="Siapa pemilik produk ini?"
|
placeholder="Siapa pemilik produk ini?"
|
||||||
required
|
required
|
||||||
searchable
|
searchable
|
||||||
data={umkmState.umkm.findMany.data?.map(v => ({
|
data={umkmState.umkm.findMany.data?.map((v: any) => ({
|
||||||
value: v.id, label: v.nama
|
value: v.id, label: v.nama
|
||||||
})) || []}
|
})) || []}
|
||||||
value={formData.umkmId}
|
value={formData.umkmId}
|
||||||
@@ -327,7 +343,7 @@ function EditProdukUmkm() {
|
|||||||
label="Kategori Produk"
|
label="Kategori Produk"
|
||||||
placeholder="Pilih kategori produk"
|
placeholder="Pilih kategori produk"
|
||||||
required
|
required
|
||||||
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
|
data={umkmState.kategoriProduk.findManyAll.data?.map((v: any) => ({
|
||||||
value: v.id, label: v.nama
|
value: v.id, label: v.nama
|
||||||
})) || []}
|
})) || []}
|
||||||
value={formData.kategoriId}
|
value={formData.kategoriId}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
NumberInput
|
NumberInput
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -43,7 +43,7 @@ export default function CreateProdukUmkm() {
|
|||||||
stok: 0,
|
stok: 0,
|
||||||
umkmId: "",
|
umkmId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: null,
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export default function CreateProdukUmkm() {
|
|||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let uploadedImageId = "";
|
let uploadedImageId = null;
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import DemografiPekerjaan from "./demografi-pekerjaan";
|
|||||||
import JumlahPengangguran from "./jumlah-pengangguran";
|
import JumlahPengangguran from "./jumlah-pengangguran";
|
||||||
import PendapatanAsliDesa from "./pendapatan-asli-desa";
|
import PendapatanAsliDesa from "./pendapatan-asli-desa";
|
||||||
import StrukturOrganisasi from "./struktur-bumdes";
|
import StrukturOrganisasi from "./struktur-bumdes";
|
||||||
import KategoriProduk from "./kategori-produk";
|
import KategoriProduk from "./umkm/kategori-produk/kategori-produk";
|
||||||
import Umkm from "./umkm";
|
import Umkm from "./umkm";
|
||||||
import ProdukUmkm from "./umkm/produk";
|
import ProdukUmkm from "./umkm/produk";
|
||||||
import PenjualanProduk from "./umkm/penjualan";
|
import PenjualanProduk from "./umkm/penjualan";
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import Elysia from "elysia";
|
|
||||||
|
|
||||||
const KategoriProduk = new Elysia({
|
|
||||||
prefix: "/kategoriproduk",
|
|
||||||
})
|
|
||||||
.get("/find-many-all", async () => {
|
|
||||||
try {
|
|
||||||
const data = await prisma.kategoriProduk.findMany({
|
|
||||||
where: {
|
|
||||||
isActive: true,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
orderBy: { nama: 'asc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Berhasil mengambil semua kategori produk",
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error di KategoriProduk find-many-all:", e);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Gagal mengambil data kategori produk",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default KategoriProduk;
|
|
||||||
@@ -2,33 +2,97 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
async function umkmDashboardKpi(context: Context) {
|
async function umkmDashboardKpi(context: Context) {
|
||||||
const periode = (context.query.periode as string) ||
|
const periode =
|
||||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
(context.query.periode as string) ||
|
||||||
|
`${new Date().getFullYear()}-${String(
|
||||||
|
new Date().getMonth() + 1
|
||||||
|
).padStart(2, "0")}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [umkmAktif, totalUmkm, omzetBulanan, kategoriTerbanyak] = await Promise.all([
|
// KPI utama
|
||||||
prisma.umkm.count({ where: { isActive: true, deletedAt: null } }),
|
const [umkmAktif, totalUmkm, omzetBulanan] = await Promise.all([
|
||||||
prisma.umkm.count({ where: { deletedAt: null } }),
|
prisma.umkm.count({
|
||||||
|
where: { isActive: true, deletedAt: null },
|
||||||
|
}),
|
||||||
|
prisma.umkm.count({
|
||||||
|
where: { deletedAt: null },
|
||||||
|
}),
|
||||||
prisma.penjualanProduk.aggregate({
|
prisma.penjualanProduk.aggregate({
|
||||||
where: { periode, deletedAt: null },
|
where: { periode, deletedAt: null },
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true },
|
||||||
}),
|
}),
|
||||||
prisma.umkm.groupBy({
|
|
||||||
by: ['kategoriId'],
|
|
||||||
_count: { _all: true },
|
|
||||||
orderBy: { _count: { kategoriId: 'desc' } },
|
|
||||||
take: 1
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Ambil nama kategori jika ada
|
// =========================
|
||||||
|
// 1. Cari kategori dari penjualan
|
||||||
|
// =========================
|
||||||
|
const salesByCategory = await prisma.penjualanProduk.findMany({
|
||||||
|
where: { periode, deletedAt: null },
|
||||||
|
select: {
|
||||||
|
jumlah: true,
|
||||||
|
produk: {
|
||||||
|
select: {
|
||||||
|
kategoriProdukId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let kategoriNama = "-";
|
let kategoriNama = "-";
|
||||||
if (kategoriTerbanyak.length > 0) {
|
|
||||||
const kat = await prisma.kategoriProduk.findUnique({
|
if (salesByCategory.length > 0) {
|
||||||
where: { id: kategoriTerbanyak[0].kategoriId },
|
const categoryCounts: Record<string, number> = {};
|
||||||
select: { nama: true }
|
|
||||||
|
for (const sale of salesByCategory) {
|
||||||
|
const catId = sale.produk.kategoriProdukId;
|
||||||
|
if (!catId) continue;
|
||||||
|
|
||||||
|
categoryCounts[catId] =
|
||||||
|
(categoryCounts[catId] || 0) + sale.jumlah;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cari kategori dengan penjualan tertinggi
|
||||||
|
let topCategoryId: string | null = null;
|
||||||
|
let maxSales = 0;
|
||||||
|
|
||||||
|
for (const [id, count] of Object.entries(categoryCounts)) {
|
||||||
|
if (count > maxSales) {
|
||||||
|
maxSales = count;
|
||||||
|
topCategoryId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topCategoryId) {
|
||||||
|
const kategori = await prisma.kategoriProduk.findUnique({
|
||||||
|
where: { id: topCategoryId },
|
||||||
|
select: { nama: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
kategoriNama = kategori?.nama || "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 2. Fallback (kalau tidak ada penjualan)
|
||||||
|
// =========================
|
||||||
|
if (kategoriNama === "-") {
|
||||||
|
const kategoriTerbanyakUmkm = await prisma.umkm.groupBy({
|
||||||
|
by: ["kategoriId"],
|
||||||
|
_count: { _all: true },
|
||||||
|
orderBy: {
|
||||||
|
_count: { kategoriId: "desc" },
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
});
|
});
|
||||||
kategoriNama = kat?.nama || "-";
|
|
||||||
|
if (kategoriTerbanyakUmkm.length > 0) {
|
||||||
|
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
||||||
|
where: { id: kategoriTerbanyakUmkm[0].kategoriId },
|
||||||
|
select: { nama: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
kategoriNama = kategori?.nama || "-";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -37,13 +101,16 @@ async function umkmDashboardKpi(context: Context) {
|
|||||||
umkmAktif,
|
umkmAktif,
|
||||||
totalUmkm,
|
totalUmkm,
|
||||||
omzetBulanan: omzetBulanan._sum.totalNilai || 0,
|
omzetBulanan: omzetBulanan._sum.totalNilai || 0,
|
||||||
kategoriTerbanyak: kategoriNama
|
kategoriTerbanyak: kategoriNama,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error di umkmDashboardKpi:", e);
|
console.error("Error di umkmDashboardKpi:", e);
|
||||||
return { success: false, message: "Gagal mengambil data KPI dashboard" };
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data KPI dashboard",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default umkmDashboardKpi;
|
export default umkmDashboardKpi;
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
|
||||||
|
const KategoriProduk = new Elysia({
|
||||||
|
prefix: "/kategoriproduk",
|
||||||
|
})
|
||||||
|
.get("/find-many-all", async () => {
|
||||||
|
try {
|
||||||
|
const data = await prisma.kategoriProdukUmkm.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
orderBy: { nama: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil mengambil semua kategori produk",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di KategoriProduk find-many-all:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data kategori produk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/find-many", async ({ query }) => {
|
||||||
|
try {
|
||||||
|
const { page = 1, limit = 10, search = "" } = query;
|
||||||
|
const skip = (Number(page) - 1) * Number(limit);
|
||||||
|
const take = Number(limit);
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
nama: { contains: search, mode: 'insensitive' as const },
|
||||||
|
};
|
||||||
|
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.kategoriProdukUmkm.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
prisma.kategoriProdukUmkm.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil mengambil data kategori produk",
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalPages: Math.ceil(total / take),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di KategoriProduk find-many:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data kategori produk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
page: t.Optional(t.String()),
|
||||||
|
limit: t.Optional(t.String()),
|
||||||
|
search: t.Optional(t.String()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post("/create", async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const data = await prisma.kategoriProdukUmkm.create({
|
||||||
|
data: {
|
||||||
|
nama: body.nama,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil membuat kategori produk",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di KategoriProduk create:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal membuat kategori produk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
nama: t.String(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.put("/:id", async ({ params, body }) => {
|
||||||
|
try {
|
||||||
|
const data = await prisma.kategoriProdukUmkm.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: {
|
||||||
|
nama: body.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil memperbarui kategori produk",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di KategoriProduk update:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal memperbarui kategori produk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
nama: t.String(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.delete("/del/:id", async ({ params }) => {
|
||||||
|
try {
|
||||||
|
const data = await prisma.kategoriProdukUmkm.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: {
|
||||||
|
isActive: false,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil menghapus kategori produk",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di KategoriProduk delete:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal menghapus kategori produk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export default KategoriProduk;
|
||||||
@@ -13,7 +13,21 @@ async function penjualanProdukCreate(context: Context) {
|
|||||||
try {
|
try {
|
||||||
// Gunakan transaction untuk update stok produk (PasarDesa)
|
// Gunakan transaction untuk update stok produk (PasarDesa)
|
||||||
const result = await prisma.$transaction(async (tx) => {
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
// 1. Catat penjualan (relasi ke PasarDesa)
|
// 1. Validasi stok produk
|
||||||
|
const produk = await tx.pasarDesa.findUnique({
|
||||||
|
where: { id: body.produkId },
|
||||||
|
select: { stok: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!produk) {
|
||||||
|
throw new Error("Produk tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (produk.stok < body.jumlah) {
|
||||||
|
throw new Error(`Stok tidak mencukupi. Tersedia: ${produk.stok}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Catat penjualan (relasi ke PasarDesa)
|
||||||
const penjualan = await tx.penjualanProduk.create({
|
const penjualan = await tx.penjualanProduk.create({
|
||||||
data: {
|
data: {
|
||||||
produkId: body.produkId,
|
produkId: body.produkId,
|
||||||
@@ -26,7 +40,7 @@ async function penjualanProdukCreate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Update stok di model PasarDesa
|
// 3. Update stok di model PasarDesa
|
||||||
await tx.pasarDesa.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: body.produkId },
|
where: { id: body.produkId },
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ const ApiServer = new Elysia()
|
|||||||
.group("/api", (app) =>
|
.group("/api", (app) =>
|
||||||
app
|
app
|
||||||
.use(Utils)
|
.use(Utils)
|
||||||
|
.get("/version", async () => {
|
||||||
|
const packageJson = await fs.readFile(
|
||||||
|
path.join(ROOT, "package.json"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
return { version: JSON.parse(packageJson).version };
|
||||||
|
})
|
||||||
.use(FileStorage)
|
.use(FileStorage)
|
||||||
.use(LandingPage)
|
.use(LandingPage)
|
||||||
.use(PPID)
|
.use(PPID)
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ function DetailLowonganKerjaUser() {
|
|||||||
size="md"
|
size="md"
|
||||||
mt="md"
|
mt="md"
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
onClick={() => window.open(`https://wa.me/${data.notelp}`, '_blank')}
|
onClick={() => window.open(`https://wa.me/${data.notelp?.replace(/[^0-9]/g, '').replace(/^0/, '62')}`, '_blank')}
|
||||||
leftSection={<IconBrandWhatsapp size={20} />}
|
leftSection={<IconBrandWhatsapp size={20} />}
|
||||||
>
|
>
|
||||||
Hubungi Sekarang
|
Hubungi Sekarang
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function Page() {
|
|||||||
color="green"
|
color="green"
|
||||||
radius="md"
|
radius="md"
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://wa.me/${u.kontak}`}
|
href={`https://wa.me/${u.kontak?.replace(/[^0-9]/g, '').replace(/^0/, '62')}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
w="fit-content"
|
w="fit-content"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,132 +1,227 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider, Title } from '@mantine/core';
|
import React, { useState } from 'react';
|
||||||
import { IconArrowBack, IconBrandWhatsapp, IconMapPin, IconUser } from '@tabler/icons-react';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
Skeleton,
|
||||||
|
Group,
|
||||||
|
Badge,
|
||||||
|
Divider,
|
||||||
|
Title,
|
||||||
|
Modal,
|
||||||
|
NumberInput,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Alert,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconArrowBack,
|
||||||
|
IconBrandWhatsapp,
|
||||||
|
IconMapPin,
|
||||||
|
IconUser,
|
||||||
|
IconShoppingCart,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import React from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import colors from '@/con/colors';
|
||||||
import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm';
|
import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
|
||||||
|
interface OrderForm {
|
||||||
|
nama: string;
|
||||||
|
jumlah: number;
|
||||||
|
catatan: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FORM: OrderForm = {
|
||||||
|
nama: '',
|
||||||
|
jumlah: 1,
|
||||||
|
catatan: '',
|
||||||
|
};
|
||||||
|
|
||||||
function DetailProdukPasarUser() {
|
function DetailProdukPasarUser() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const state = useProxy(umkmState.produk.findUnique);
|
const state = useProxy(umkmState.produk.findUnique);
|
||||||
|
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form, setForm] = useState<OrderForm>(DEFAULT_FORM);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.load(params?.id as string);
|
state.load(params?.id as string);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const data = state.data;
|
const data = state.data;
|
||||||
|
const total = data ? form.jumlah * (data.harga ?? 0) : 0;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setModalOpen(false);
|
||||||
|
setError('');
|
||||||
|
setForm(DEFAULT_FORM);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrder = async () => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
if (!form.nama.trim()) {
|
||||||
|
setError('Nama pemesan wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.jumlah < 1) {
|
||||||
|
setError('Jumlah minimal 1');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.jumlah > data.stok) {
|
||||||
|
setError(`Stok tersedia hanya ${data.stok}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.ekonomi.umkm.penjualan.create.post({
|
||||||
|
produkId: data.id,
|
||||||
|
jumlah: form.jumlah,
|
||||||
|
hargaSatuan: data.harga || 0,
|
||||||
|
tanggal: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.data?.success) {
|
||||||
|
throw new Error(res.data?.message || 'Gagal membuat pesanan');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.load(params?.id as string);
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
toast.success('Pesanan berhasil dicatat!');
|
||||||
|
|
||||||
|
let kontak = data.umkm?.kontak?.replace(/[^0-9]/g, '') || '';
|
||||||
|
if (kontak.startsWith('0')) {
|
||||||
|
kontak = '62' + kontak.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kontak) {
|
||||||
|
const message = [
|
||||||
|
`Halo *${data.umkm?.nama}*, saya ingin memesan:`,
|
||||||
|
'',
|
||||||
|
`*${data.nama}*`,
|
||||||
|
`Jumlah: ${form.jumlah}`,
|
||||||
|
`Harga Satuan: Rp ${data.harga?.toLocaleString('id-ID')}`,
|
||||||
|
`*Total: Rp ${total.toLocaleString('id-ID')}*`,
|
||||||
|
'',
|
||||||
|
`Nama Pemesan: ${form.nama}`,
|
||||||
|
form.catatan ? `Catatan: ${form.catatan}` : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`https://wa.me/${kontak}?text=${encodeURIComponent(message)}`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (state.loading || !data) {
|
if (state.loading || !data) {
|
||||||
return (
|
return <Skeleton h={400} />;
|
||||||
<Stack py={10} px={{ base: 'md', md: 100 }}>
|
|
||||||
<Skeleton height={400} radius="md" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={20} bg={colors.Bg}>
|
<Box py={20} bg={colors.Bg}>
|
||||||
{/* Tombol kembali */}
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.push('/darmasaba/ekonomi/umkm')}
|
leftSection={<IconArrowBack size={16} />}
|
||||||
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
onClick={() => router.back()}
|
||||||
mb={15}
|
|
||||||
>
|
>
|
||||||
<Text fz={{ base: 'md', md: 'lg' }} lh={1.5}>
|
Kembali
|
||||||
Kembali ke Katalog
|
|
||||||
</Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper
|
<Paper mt="md" mx={{ base: 'md', md: 100 }} p="lg" radius="md">
|
||||||
w={{ base: '90%', md: '70%' }}
|
<Stack>
|
||||||
mx="auto"
|
|
||||||
p="lg"
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
bg="white"
|
|
||||||
>
|
|
||||||
<Stack gap="lg">
|
|
||||||
{/* Gambar Produk */}
|
|
||||||
<Image
|
<Image
|
||||||
src={data.image?.link || '/no-image.jpg'}
|
src={data.image?.link || '/no-image.jpg'}
|
||||||
alt={data.nama}
|
alt={data.nama}
|
||||||
radius="md"
|
radius="md"
|
||||||
h={{ base: 250, md: 400 }}
|
|
||||||
w="100%"
|
|
||||||
fit="cover"
|
|
||||||
fallbackSrc="/no-image.jpg"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Detail Produk */}
|
<Title>{data.nama}</Title>
|
||||||
<Stack gap="xs">
|
|
||||||
<Group justify="space-between" align="flex-start">
|
|
||||||
<Stack gap={5}>
|
|
||||||
<Badge color="blue" variant="light">{data.kategoriProduk?.nama}</Badge>
|
|
||||||
<Title order={1} fw={800} c={colors['blue-button']}>
|
|
||||||
{data.nama}
|
|
||||||
</Title>
|
|
||||||
</Stack>
|
|
||||||
<Badge color={data.stok > 0 ? 'green' : 'red'} size="lg">
|
|
||||||
{data.stok > 0 ? `Stok: ${data.stok}` : 'Stok Habis'}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Text fz="2rem" fw={900} c="orange">
|
|
||||||
Rp {data.harga?.toLocaleString('id-ID')}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Divider my="sm" />
|
<Text fw={900} c="orange">
|
||||||
|
Rp {data.harga?.toLocaleString('id-ID')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Stack gap="md">
|
<Badge color={data.stok > 0 ? 'green' : 'red'}>
|
||||||
<Box>
|
{data.stok > 0 ? `Stok: ${data.stok}` : 'Stok Habis'}
|
||||||
<Title order={3} mb="xs">Informasi Penjual</Title>
|
</Badge>
|
||||||
<Paper withBorder p="md" radius="md" bg="gray.0">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Stack gap={4}>
|
|
||||||
<Text fw={700} fz="lg" c="blue" style={{ cursor: 'pointer' }} onClick={() => router.push(`/darmasaba/ekonomi/umkm/${data.umkmId}`)}>
|
|
||||||
{data.umkm?.nama}
|
|
||||||
</Text>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconUser size={16} color="gray" />
|
|
||||||
<Text size="sm" c="dimmed">{data.umkm?.pemilik}</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMapPin size={16} color="red" />
|
|
||||||
<Text size="sm" c="dimmed">{data.umkm?.alamat || 'Darmasaba'}</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
{data.umkm?.kontak && (
|
|
||||||
<Button
|
|
||||||
color="green"
|
|
||||||
radius="md"
|
|
||||||
component="a"
|
|
||||||
href={`https://wa.me/${data.umkm.kontak.replace(/[^0-9]/g, '')}`}
|
|
||||||
target="_blank"
|
|
||||||
leftSection={<IconBrandWhatsapp size={20}/>}
|
|
||||||
>
|
|
||||||
WhatsApp
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
<Text>{data.deskripsi}</Text>
|
||||||
<Title order={3} mb="xs">Deskripsi Produk</Title>
|
|
||||||
<Text fz="md" lh={1.6} c="dark">
|
<Button
|
||||||
{data.deskripsi || 'Tidak ada deskripsi tersedia untuk produk ini.'}
|
leftSection={<IconShoppingCart />}
|
||||||
</Text>
|
disabled={data.stok === 0}
|
||||||
</Box>
|
onClick={() => setModalOpen(true)}
|
||||||
</Stack>
|
>
|
||||||
</Stack>
|
Pesan Sekarang
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<Modal opened={modalOpen} onClose={handleClose} title="Pesanan">
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
label="Nama"
|
||||||
|
value={form.nama}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({ ...form, nama: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label="Jumlah"
|
||||||
|
min={1}
|
||||||
|
max={data.stok}
|
||||||
|
value={form.jumlah}
|
||||||
|
onChange={(v) =>
|
||||||
|
setForm({ ...form, jumlah: Number(v) || 1 })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Catatan"
|
||||||
|
value={form.catatan}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({ ...form, catatan: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <Alert color="red">{error}</Alert>}
|
||||||
|
|
||||||
|
<Button loading={loading} onClick={handleOrder}>
|
||||||
|
Kirim via WhatsApp
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ function Page() {
|
|||||||
<Button variant="light" leftSection={<IconDeviceLandlinePhone size={18} />} component="a" href={`tel:${kontak.telepon}`} aria-label="Hubungi Telepon">
|
<Button variant="light" leftSection={<IconDeviceLandlinePhone size={18} />} component="a" href={`tel:${kontak.telepon}`} aria-label="Hubungi Telepon">
|
||||||
<Text fz={{ base: 'xs', md: 'sm' }}>Telepon</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }}>Telepon</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">
|
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '').replace(/^0/, '62')}`} target="_blank" aria-label="Hubungi WhatsApp">
|
||||||
<Text fz={{ base: 'xs', md: 'sm' }}>WhatsApp</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }}>WhatsApp</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">
|
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Client } from "minio";
|
import { Client } from "minio";
|
||||||
|
|
||||||
const minioClient = new Client({
|
const minioClient = new Client({
|
||||||
endPoint: process.env.MINIO_ENDPOINT!,
|
endPoint: process.env.MINIO_ENDPOINT ?? "localhost",
|
||||||
accessKey: process.env.MINIO_ACCESS_KEY!,
|
accessKey: process.env.MINIO_ACCESS_KEY ?? "",
|
||||||
secretKey: process.env.MINIO_SECRET_KEY!,
|
secretKey: process.env.MINIO_SECRET_KEY ?? "",
|
||||||
useSSL: process.env.MINIO_USE_SSL === "true",
|
useSSL: process.env.MINIO_USE_SSL === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MINIO_BUCKET = process.env.MINIO_BUCKET!;
|
export const MINIO_BUCKET = process.env.MINIO_BUCKET ?? "";
|
||||||
|
|
||||||
export default minioClient;
|
export default minioClient;
|
||||||
|
|||||||
Reference in New Issue
Block a user