Compare commits
76 Commits
fix-tombol
...
backup-stg
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b62e01556 | |||
|
|
0dabc204bc | ||
|
|
e8f8b51686 | ||
|
|
a4db3a149d | ||
|
|
fece983ac5 | ||
| 8b7eef5fee | |||
| 8b22d01e0d | |||
| dc13e37a02 | |||
| 2d2cbef29b | |||
| 8c8a96b830 | |||
| dc3eccacbf | |||
| ffe94992e5 | |||
| 4fb522f88f | |||
| 85332a8225 | |||
| 3fe2a5ccab | |||
| 363bfa65fb | |||
| dccf590cbf | |||
| f076b81d14 | |||
| b5ea3216e0 | |||
| 64b116588b | |||
| 63161e1a39 | |||
| 4a7811e06f | |||
| 3803c79c95 | |||
| 0160fa636d | |||
| 3684e83187 | |||
| 77c54b5c8a | |||
| bb80b0ecc1 | |||
| 1b59d6bf09 | |||
| eb1ad54db6 | |||
| 21ec3ad1c1 | |||
| 3a115908c4 | |||
| 5ff791642c | |||
| b803c7a90c | |||
| fb2fe67c23 | |||
| 51460558d4 | |||
| d105ceeb6b | |||
| c865aee766 | |||
| 273dfdfd09 | |||
| 1d1d8e50dc | |||
| 092afe67d2 | |||
| 2d9170705d | |||
| fdf9a951a4 | |||
| ca74029688 | |||
| 1a8fc1a670 | |||
| 19235f0791 | |||
| 61de7d8d33 | |||
| 8fb85ce56c | |||
| 1f98b6993d | |||
| f3a10d63d1 | |||
| 7a42bec63b | |||
| 44c421129e | |||
| ddff427926 | |||
| 00c8caade4 | |||
| 0209f49449 | |||
| 344c6ada6d | |||
| 11acd04419 | |||
| 8d49213b68 | |||
| 96911e3cf1 | |||
| 9950c28b9b | |||
| fa0f3538d1 | |||
| 2778f53aff | |||
| 37ac91d4f4 | |||
| 217f4a9a3b | |||
| 5d6a7437ed | |||
| 752a6cabee | |||
| 134ddc6154 | |||
| 28979c6b49 | |||
| b2066caa13 | |||
| 023c77d636 | |||
| 9bf3ec72cf | |||
| f359f5b1ce | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 |
19
.env
Normal file
19
.env
Normal file
@@ -0,0 +1,19 @@
|
||||
DATABASE_URL="postgresql://bip:Production_123@localhost:5433/desa-darmasaba-v0.0.1?schema=public"
|
||||
# Seafile
|
||||
SEAFILE_TOKEN=20a19f4a04032215d50ce53292e6abdd38b9f806
|
||||
SEAFILE_REPO_ID=f0e9ee4a-fd13-49a2-81c0-f253951d063a
|
||||
SEAFILE_URL=https://cld-dkr-makuro-seafile.wibudev.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=3a9a9ecb5e244f4da8ae
|
||||
|
||||
# Upload
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR="./download"
|
||||
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
|
||||
EMAIL_USER=nicoarya20@gmail.com
|
||||
EMAIL_PASS=hymmfpcaqzqkfgbh
|
||||
BASE_SESSION_KEY=kp9sGx91as0Kj2Ls81nAsl2Kdj13KsxP
|
||||
BASE_TOKEN_KEY=Qm82JsA92lMnKw0291mxKaaP02KjslaA
|
||||
|
||||
# BOT-TELE
|
||||
BOT_TOKEN=8498428675:AAEQwAUjTqpvgyyC5C123nP1mAxhOg12Ph0
|
||||
CHAT_ID=5251328671
|
||||
36
.env.example
Normal file
36
.env.example
Normal file
@@ -0,0 +1,36 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
|
||||
|
||||
# Seafile Configuration (File Storage)
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_URL=https://your-seafile-instance.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
|
||||
# Upload Configuration
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR=./download
|
||||
|
||||
# Application Configuration
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
|
||||
# Email Configuration (for notifications/subscriptions)
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_app_password
|
||||
|
||||
# Session Configuration
|
||||
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
|
||||
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
|
||||
|
||||
# Telegram Bot Configuration (for notifications)
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
CHAT_ID=your_telegram_chat_id
|
||||
|
||||
# Session Password (for iron-session)
|
||||
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
||||
|
||||
# ElevenLabs API Key (for TTS features - optional)
|
||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||
|
||||
# Environment (optional, defaults to development)
|
||||
# NODE_ENV=development
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env
|
||||
.env*
|
||||
# env local files (keep .env.example)
|
||||
.env.local
|
||||
.env*.local
|
||||
.env.production
|
||||
.env.development
|
||||
!.env.example
|
||||
|
||||
# QC
|
||||
QC
|
||||
@@ -52,7 +57,5 @@ next-env.d.ts
|
||||
|
||||
.github/
|
||||
|
||||
.env.*
|
||||
|
||||
*.tar.gz
|
||||
|
||||
|
||||
73
AUDIT_REPORT.md
Normal file
73
AUDIT_REPORT.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Engineering Audit Report: Desa Darmasaba
|
||||
**Status:** Production Readiness Review (Critical)
|
||||
**Auditor:** Staff Technical Architect
|
||||
|
||||
---
|
||||
|
||||
## 📊 Executive Summary & Scores
|
||||
|
||||
| Category | Score | Status |
|
||||
| :--- | :---: | :--- |
|
||||
| **Project Architecture** | 3/10 | 🔴 Critical Failure |
|
||||
| **Code Quality** | 4/10 | 🟠 Poor |
|
||||
| **Performance** | 5/10 | 🟡 Mediocre |
|
||||
| **Security** | 5/10 | 🟠 Risk Detected |
|
||||
| **Production Readiness** | 2/10 | 🔴 Not Ready |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 1. Project Architecture
|
||||
The project suffers from a **"Frankenstein Architecture"**. It attempts to run a full Elysia.js instance inside a Next.js Catch-All route.
|
||||
- **Fractured Backend:** Logic is split between standard Next.js routes (`/api/auth`) and embedded Elysia modules.
|
||||
- **Stateful Dependency:** Reliance on local filesystem (`WIBU_UPLOAD_DIR`) makes the application impossible to deploy on modern serverless platforms like Vercel.
|
||||
- **Polluted Namespace:** Routing tree contains "test/coba" folders (`src/app/coba`, `src/app/percobaan`) that would be accessible in production.
|
||||
|
||||
## ⚛️ 2. Frontend Engineering (React / Next.js)
|
||||
- **State Management Chaos:** Simultaneous use of `Valtio`, `Jotai`, `React Context`, and `localStorage`.
|
||||
- **Tight Coupling:** Public pages (`/darmasaba`) import state directly from Admin internal states (`/admin/(dashboard)/_state`).
|
||||
- **Heavy Client-Side Logic:** Logic that belongs in Server Actions or Hooks is embedded in presentational components (e.g., `Footer.tsx`).
|
||||
|
||||
## 📡 3. Backend / API Design
|
||||
- **Framework Overhead:** Running Elysia inside Next.js adds unnecessary cold-boot overhead and complexity.
|
||||
- **Weak Validation:** Widespread use of `as Type` casting in API handlers instead of runtime validation (Zod/Schema).
|
||||
- **Service Integration:** OTP codes are sent via external `GET` requests with sensitive data in the query string—a major logging risk.
|
||||
|
||||
## 🗄️ 4. Database & Data Modeling (Prisma)
|
||||
- **Schema Over-Normalization:** ~2000 lines of schema. Every minor content type (e.g., `LambangDesa`) is a separate table instead of a unified CMS model.
|
||||
- **Polymorphic Monolith:** `FileStorage` is a "god table" with optional relations to ~40 other tables, creating a massive bottleneck and data integrity risk.
|
||||
- **Connection Mismanagement:** Manual `prisma.$disconnect()` in API routes kills connection pooling performance.
|
||||
|
||||
## 🚀 5. Performance Engineering
|
||||
- **Bypassing Optimization:** Custom `/api/utils/img` endpoint bypasses `next/image` optimization, serving uncompressed assets.
|
||||
- **Aggressive Polling:** Client-side 30s polling for notifications is battery-draining and inefficient compared to SSE or SWR.
|
||||
|
||||
## 🔒 6. Security Audit
|
||||
- **Insecure OTP Delivery:** Credentials passed as URL parameters to the WhatsApp service.
|
||||
- **File Upload Risks:** Potential for Arbitrary File Upload due to direct local filesystem writes without rigorous sanitization.
|
||||
|
||||
## 🧹 7. Code Quality
|
||||
- **Inconsistency:** Mixed English/Indonesian naming (e.g., `nomor` vs `createdAt`).
|
||||
- **Artifacts:** Root directory is littered with scratch files: `xcoba.ts`, `xx.ts`, `test.txt`.
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Top 10 Critical Problems
|
||||
1. **Architectural Fracture:** Embedding Elysia inside Next.js creates a "split-brain" system.
|
||||
2. **Serverless Incompatibility:** Dependency on local disk storage for uploads.
|
||||
3. **Database Bloat:** Over-complicated schema with a fragile `FileStorage` monolith.
|
||||
4. **State Fragmentation:** Mixed usage of Jotai and Valtio without a clear standard.
|
||||
5. **Credential Leakage:** OTP codes sent via GET query parameters.
|
||||
6. **Poor Cleanup:** Trial/Test folders and files committed to the production source.
|
||||
7. **Asset Performance:** Bypassing Next.js image optimization.
|
||||
8. **Coupling:** High dependency between public UI and internal Admin state.
|
||||
9. **Type Safety:** Manual casting in APIs instead of runtime validation.
|
||||
10. **Connection Pooling:** Inefficient Prisma connection management.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Lead Refactoring Priorities
|
||||
1. **Unify the API:** Decommission the Elysia wrapper. Port all logic to standard Next.js Route Handlers with Zod validation.
|
||||
2. **Stateless Storage:** Implement an S3-compatible adapter for all file uploads. Remove `fs` usage.
|
||||
3. **Schema Consolidation:** Refactor the schema to use generic content models where possible.
|
||||
4. **Standardize State:** Choose one global state manager and migrate all components.
|
||||
5. **Project Sanitization:** Delete all `coba`, `percobaan`, and scratch files (`xcoba.ts`, etc.).
|
||||
60
Dockerfile
Normal file
60
Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# Stage 1: Build
|
||||
FROM oven/bun:1.3 AS build
|
||||
|
||||
# Install build dependencies for native modules
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Use .env.example as default env for build
|
||||
RUN cp .env.example .env
|
||||
|
||||
# Generate Prisma client
|
||||
RUN bun x prisma generate
|
||||
|
||||
# Build the application frontend
|
||||
ENV NODE_ENV=production
|
||||
RUN bun run build
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM oven/bun:1.3-slim AS runtime
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy necessary files from build stage
|
||||
COPY --from=build /app/package.json ./
|
||||
COPY --from=build /app/tsconfig.json ./
|
||||
COPY --from=build /app/.next ./.next
|
||||
COPY --from=build /app/public ./public
|
||||
COPY --from=build /app/src ./src
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/prisma ./prisma
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "start"]
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||
experimental: {},
|
||||
allowedDevOrigins: [
|
||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@mantine/modals": "^8.3.6",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@paljs/types": "^8.1.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
"@prisma/client": "6.3.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-link": "^2.11.7",
|
||||
@@ -89,7 +89,7 @@
|
||||
"p-limit": "^6.2.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
"prisma": "^6.3.1",
|
||||
"prisma": "6.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-exif-orientation-img": "^0.1.5",
|
||||
|
||||
@@ -211,6 +211,9 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
|
||||
@@ -402,6 +402,7 @@ export default function CreateMusik() {
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
|
||||
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// src/app/admin/_com/getMenuIdsByRoleId.ts
|
||||
import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||
|
||||
/**
|
||||
* Mengembalikan daftar ID menu (string[]) berdasarkan roleId
|
||||
*/
|
||||
export function getMenuIdsByRoleId(roleId: string | number): string[] {
|
||||
const id = typeof roleId === 'string' ? parseInt(roleId, 10) : roleId;
|
||||
|
||||
switch (id) {
|
||||
case 0:
|
||||
// Asumsikan devBar ada dan punya struktur sama
|
||||
return []; // atau sesuaikan jika ada devBar
|
||||
case 1:
|
||||
return navBar.map(section => section.id);
|
||||
case 2:
|
||||
return role1.map(section => section.id);
|
||||
case 3:
|
||||
return role2.map(section => section.id);
|
||||
case 4:
|
||||
return role3.map(section => section.id);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
import { assignParentIdsToApbdesItems } from "./lib/getParentsID";
|
||||
import { RealisasiItem } from "@prisma/client";
|
||||
|
||||
type APBDesItemInput = {
|
||||
kode: string;
|
||||
@@ -49,9 +50,9 @@ export default async function apbdesUpdate(context: Context) {
|
||||
}
|
||||
|
||||
// 2. Build map untuk preserve realisasiItems berdasarkan kode
|
||||
const existingItemsMap = new Map<string, {
|
||||
id: string;
|
||||
realisasiItems: any[];
|
||||
const existingItemsMap = new Map<string, {
|
||||
id: string;
|
||||
realisasiItems: RealisasiItem[];
|
||||
}>();
|
||||
|
||||
existing.items.forEach(item => {
|
||||
@@ -128,7 +129,7 @@ export default async function apbdesUpdate(context: Context) {
|
||||
}
|
||||
|
||||
// 8. Recalculate totalRealisasi setelah re-create realisasiItems
|
||||
for (const [kode, _] of existingItemsMap.entries()) {
|
||||
for (const kode of existingItemsMap.keys()) {
|
||||
const newItemId = newItemIdsMap.get(kode);
|
||||
if (newItemId) {
|
||||
const realisasiItems = await prisma.realisasiItem.findMany({
|
||||
@@ -168,8 +169,8 @@ export default async function apbdesUpdate(context: Context) {
|
||||
name: body.name || `APBDes Tahun ${body.tahun}`,
|
||||
deskripsi: body.deskripsi,
|
||||
jumlah: body.jumlah,
|
||||
imageId: body.imageId,
|
||||
fileId: body.fileId,
|
||||
imageId: body.imageId === '' ? null : body.imageId,
|
||||
fileId: body.fileId === '' ? null : body.fileId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { getMenuIdsByRoleId } from "@/app/admin/(dashboard)/user&role/_com/getMenuIdByRole";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
@@ -34,11 +35,25 @@ export default async function userUpdate(context: Context) {
|
||||
const isActiveChanged =
|
||||
isActive !== undefined && currentUser.isActive !== isActive;
|
||||
|
||||
// ✅ Jika role berubah, hapus semua akses menu yang ada
|
||||
if (isRoleChanged) {
|
||||
// ✅ Jika role berubah, reset dan set ulang akses menu
|
||||
if (isRoleChanged && roleId) {
|
||||
// Hapus akses lama
|
||||
await prisma.userMenuAccess.deleteMany({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
// Ambil menu default untuk role baru
|
||||
const menuIds = getMenuIdsByRoleId(roleId);
|
||||
|
||||
if (menuIds.length > 0) {
|
||||
// Buat akses baru
|
||||
await prisma.userMenuAccess.createMany({
|
||||
data: menuIds.map(menuId => ({
|
||||
userId: id,
|
||||
menuId
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update user
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Paper, Title, Progress, Stack, Text, Group, Box } from '@mantine/core';
|
||||
|
||||
function Summary({ title, data }: any) {
|
||||
interface APBDesItem {
|
||||
tipe: string | null;
|
||||
anggaran: number;
|
||||
realisasi?: number;
|
||||
totalRealisasi?: number;
|
||||
}
|
||||
|
||||
interface SummaryProps {
|
||||
title: string;
|
||||
data: APBDesItem[];
|
||||
}
|
||||
|
||||
function Summary({ title, data }: SummaryProps) {
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
const totalAnggaran = data.reduce((s: number, i: any) => s + i.anggaran, 0);
|
||||
const totalAnggaran = data.reduce((s: number, i: APBDesItem) => s + i.anggaran, 0);
|
||||
// Use realisasi field (already mapped from totalRealisasi in transformAPBDesData)
|
||||
const totalRealisasi = data.reduce((s: number, i: any) => s + (i.realisasi || i.totalRealisasi || 0), 0);
|
||||
const totalRealisasi = data.reduce(
|
||||
(s: number, i: APBDesItem) => s + (i.realisasi || i.totalRealisasi || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const persen =
|
||||
totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
||||
@@ -78,28 +93,21 @@ function Summary({ title, data }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function GrafikRealisasi({ apbdesData }: any) {
|
||||
const items = apbdesData.items || [];
|
||||
const tahun = apbdesData.tahun || new Date().getFullYear();
|
||||
|
||||
const pendapatan = items.filter((i: any) => i.tipe === 'pendapatan');
|
||||
const belanja = items.filter((i: any) => i.tipe === 'belanja');
|
||||
const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
|
||||
|
||||
// Hitung total keseluruhan
|
||||
const totalAnggaranSemua = items.reduce((s: number, i: any) => s + i.anggaran, 0);
|
||||
// Use realisasi field (already mapped from totalRealisasi in transformAPBDesData)
|
||||
const totalRealisasiSemua = items.reduce((s: number, i: any) => s + (i.realisasi || i.totalRealisasi || 0), 0);
|
||||
const persenSemua = totalAnggaranSemua > 0 ? (totalRealisasiSemua / totalAnggaranSemua) * 100 : 0;
|
||||
|
||||
const formatRupiah = (angka: number) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(angka);
|
||||
export default function GrafikRealisasi({
|
||||
apbdesData,
|
||||
}: {
|
||||
apbdesData: {
|
||||
tahun?: number | null;
|
||||
items?: APBDesItem[] | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
}) {
|
||||
const items = apbdesData?.items || [];
|
||||
const tahun = apbdesData?.tahun || new Date().getFullYear();
|
||||
|
||||
const pendapatan = items.filter((i: APBDesItem) => i.tipe === 'pendapatan');
|
||||
const belanja = items.filter((i: APBDesItem) => i.tipe === 'belanja');
|
||||
const pembiayaan = items.filter((i: APBDesItem) => i.tipe === 'pembiayaan');
|
||||
|
||||
return (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
@@ -112,27 +120,6 @@ export default function GrafikRealisasi({ apbdesData }: any) {
|
||||
<Summary title="💸 Belanja" data={belanja} />
|
||||
<Summary title="📊 Pembiayaan" data={pembiayaan} />
|
||||
</Stack>
|
||||
|
||||
{/* Summary Total Keseluruhan
|
||||
<Box p="md" bg="gray.0">
|
||||
<>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
||||
<Text fw={700} fz="xl" c={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}>
|
||||
{persenSemua.toFixed(2)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed" mb="xs">
|
||||
{formatRupiah(totalRealisasiSemua)} / {formatRupiah(totalAnggaranSemua)}
|
||||
</Text>
|
||||
<Progress
|
||||
value={persenSemua}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}
|
||||
/>
|
||||
</>
|
||||
</Box> */}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "./globals.css";
|
||||
import "./globals.css"; // Sisanya import di globals.css
|
||||
|
||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||
import { MusicProvider } from "@/app/context/MusicContext";
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MantineProvider,
|
||||
createTheme,
|
||||
mantineHtmlProps,
|
||||
// mantineHtmlProps,
|
||||
} from "@mantine/core";
|
||||
import { Metadata, Viewport } from "next";
|
||||
import { ViewTransitions } from "next-view-transitions";
|
||||
@@ -98,13 +99,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ViewTransitions>
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
</head>
|
||||
<body>
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
</head>
|
||||
<body>
|
||||
<ViewTransitions>
|
||||
<MusicProvider>
|
||||
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||
{children}
|
||||
@@ -116,8 +117,8 @@ export default function RootLayout({
|
||||
/>
|
||||
</MantineProvider>
|
||||
</MusicProvider>
|
||||
</body>
|
||||
</html>
|
||||
</ViewTransitions>
|
||||
</ViewTransitions>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ function Page() {
|
||||
dengan ketentuan ini, harap jangan gunakan Website.
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
|
||||
<Box>
|
||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
||||
|
||||
Reference in New Issue
Block a user