Compare commits
31 Commits
main
...
nico/25-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 113dd7ba6f | |||
| 71a305cd4b | |||
| 84b96ca3be | |||
| 8159216a2c | |||
| d714c09efc | |||
| 0a97e31416 | |||
| 158a2db435 | |||
| 2d68d4dc06 | |||
| 97e6caa332 | |||
| f0c37272b9 | |||
| 8c35d58b38 | |||
| 952f7ecb16 | |||
| a74e0c02e5 | |||
| 17ecd3feca | |||
| d88cf2b100 | |||
| e0955ed2c4 | |||
| 918399bf62 | |||
| 7ce2eb6ae8 | |||
| 40772859f9 | |||
| c7b34b8c28 | |||
| 9bf73a305c | |||
| 947adc1537 | |||
| 9086e28961 | |||
| 66d207c081 | |||
| b77f6e8fa3 | |||
| 9e6734d1a5 | |||
|
|
1b9ddf0f4b | ||
| a0f440f6b3 | |||
| 1f56dd7660 | |||
| 1a2a213d0a | |||
| 1ec10fe623 |
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/dashboard_desa?schema=public"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
BETTER_AUTH_SECRET="your-secret-key-here-min-32-characters"
|
||||||
|
ADMIN_EMAIL="admin@example.com"
|
||||||
|
ADMIN_PASSWORD="admin123"
|
||||||
|
|
||||||
|
# GitHub OAuth (Optional)
|
||||||
|
GITHUB_CLIENT_ID=""
|
||||||
|
GITHUB_CLIENT_SECRET=""
|
||||||
|
|
||||||
|
# Application
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Public URL
|
||||||
|
VITE_PUBLIC_URL="http://localhost:3000"
|
||||||
106
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Publish Docker to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stack_env:
|
||||||
|
description: "stack env"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "dev"
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- prod
|
||||||
|
- stg
|
||||||
|
tag:
|
||||||
|
description: "Image tag (e.g. 1.0.0)"
|
||||||
|
required: true
|
||||||
|
default: "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||||
|
sudo docker image prune --all --force
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Checkout branch ${{ github.event.inputs.stack_env }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.stack_env }}
|
||||||
|
|
||||||
|
- name: Checkout scripts from main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
path: .ci
|
||||||
|
sparse-checkout: .github/workflows/script
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate image metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||||
|
type=raw,value=${{ github.event.inputs.stack_env }}-latest
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
no-cache: true
|
||||||
|
|
||||||
|
- name: Notify success
|
||||||
|
if: success()
|
||||||
|
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: success
|
||||||
|
NOTIFY_WORKFLOW: "Publish Docker"
|
||||||
|
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||||
|
|
||||||
|
- name: Notify failure
|
||||||
|
if: failure()
|
||||||
|
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: failure
|
||||||
|
NOTIFY_WORKFLOW: "Publish Docker"
|
||||||
|
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||||
60
.github/workflows/re-pull.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Re-Pull Docker
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stack_name:
|
||||||
|
description: "stack name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
stack_env:
|
||||||
|
description: "stack env"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "dev"
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- stg
|
||||||
|
- prod
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts from main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
sparse-checkout: .github/workflows/script
|
||||||
|
|
||||||
|
- name: Deploy ke Portainer
|
||||||
|
run: bash ./.github/workflows/script/re-pull.sh
|
||||||
|
env:
|
||||||
|
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
|
||||||
|
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
|
||||||
|
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
|
||||||
|
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
|
||||||
|
|
||||||
|
- name: Notify success
|
||||||
|
if: success()
|
||||||
|
run: bash ./.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: success
|
||||||
|
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||||
|
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||||
|
|
||||||
|
- name: Notify failure
|
||||||
|
if: failure()
|
||||||
|
run: bash ./.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: failure
|
||||||
|
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||||
|
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||||
26
.github/workflows/script/notify.sh
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
|
||||||
|
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
|
||||||
|
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
|
||||||
|
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
|
||||||
|
|
||||||
|
if [ "$NOTIFY_STATUS" = "success" ]; then
|
||||||
|
ICON="✅"
|
||||||
|
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
|
||||||
|
else
|
||||||
|
ICON="❌"
|
||||||
|
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$NOTIFY_DETAIL" ]; then
|
||||||
|
TEXT="${TEXT}
|
||||||
|
${NOTIFY_DETAIL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg chat_id "$TELEGRAM_CHAT_ID" \
|
||||||
|
--arg text "$TEXT" \
|
||||||
|
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"
|
||||||
93
.github/workflows/script/re-pull.sh
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
|
||||||
|
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
|
||||||
|
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
|
||||||
|
: "${STACK_NAME:?STACK_NAME tidak di-set}"
|
||||||
|
|
||||||
|
echo "🔐 Autentikasi ke Portainer..."
|
||||||
|
TOKEN=$(curl -s -X POST https://${PORTAINER_URL}/api/auth \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
|
||||||
|
| jq -r .jwt)
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||||
|
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Mencari stack: $STACK_NAME..."
|
||||||
|
STACK=$(curl -s -X GET https://${PORTAINER_URL}/api/stacks \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
| jq ".[] | select(.Name == \"$STACK_NAME\")")
|
||||||
|
|
||||||
|
if [ -z "$STACK" ]; then
|
||||||
|
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
|
||||||
|
echo " Pastikan nama stack sudah benar."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
STACK_ID=$(echo "$STACK" | jq -r .Id)
|
||||||
|
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
|
||||||
|
ENV=$(echo "$STACK" | jq '.Env // []')
|
||||||
|
|
||||||
|
echo "📄 Mengambil compose file..."
|
||||||
|
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
| jq -r .StackFileContent)
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg content "$STACK_FILE" \
|
||||||
|
--argjson env "$ENV" \
|
||||||
|
'{stackFileContent: $content, env: $env, pullImage: true}')
|
||||||
|
|
||||||
|
echo "🚀 Redeploying $STACK_NAME (pull latest image)..."
|
||||||
|
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
|
||||||
|
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD")
|
||||||
|
|
||||||
|
if [ "$HTTP_STATUS" != "200" ]; then
|
||||||
|
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
|
||||||
|
cat /tmp/portainer_response.json | jq .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "⏳ Menunggu container running..."
|
||||||
|
|
||||||
|
MAX_RETRY=15
|
||||||
|
COUNT=0
|
||||||
|
|
||||||
|
while [ $COUNT -lt $MAX_RETRY ]; do
|
||||||
|
sleep 5
|
||||||
|
COUNT=$((COUNT + 1))
|
||||||
|
|
||||||
|
CONTAINERS=$(curl -s -X GET \
|
||||||
|
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}")
|
||||||
|
|
||||||
|
TOTAL=$(echo "$CONTAINERS" | jq 'length')
|
||||||
|
RUNNING=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "running")] | length')
|
||||||
|
FAILED=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not))] | length')
|
||||||
|
|
||||||
|
echo "🔄 [${COUNT}/${MAX_RETRY}] Running: ${RUNNING} | Failed: ${FAILED} | Total: ${TOTAL}"
|
||||||
|
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status)"'
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt "0" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Ada container yang crash!"
|
||||||
|
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not)) | " → \(.Names[0]) | \(.Status)"'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$RUNNING" -gt "0" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✅ Stack $STACK_NAME berhasil di-redeploy dan running!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "❌ Timeout! Stack tidak kunjung running setelah $((MAX_RETRY * 5)) detik."
|
||||||
|
exit 1
|
||||||
7
.gitignore
vendored
@@ -16,6 +16,7 @@ _.log
|
|||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
|
# Only .env.example is allowed to be committed
|
||||||
.env
|
.env
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@@ -33,6 +34,12 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Dashboard-MD
|
||||||
|
Dashboard-MD
|
||||||
|
|
||||||
|
# md
|
||||||
|
*.md
|
||||||
|
|
||||||
# Playwright artifacts
|
# Playwright artifacts
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|||||||
62
Dockerfile
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Generate API types
|
||||||
|
RUN bun run gen:api
|
||||||
|
|
||||||
|
# Build the application frontend using our custom build script
|
||||||
|
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/dist ./dist
|
||||||
|
COPY --from=build /app/generated ./generated
|
||||||
|
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"]
|
||||||
168
Pengaduan-New.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
Create a modern analytics dashboard UI for a village complaint system (Pengaduan Dashboard).
|
||||||
|
|
||||||
|
Tech stack:
|
||||||
|
- React 19 + Vite (Bun runtime)
|
||||||
|
- Mantine UI (core components)
|
||||||
|
- TailwindCSS (layout & spacing only)
|
||||||
|
- Recharts (charts)
|
||||||
|
- TanStack Router
|
||||||
|
- Icons: lucide-react
|
||||||
|
- State: Valtio
|
||||||
|
- Date: dayjs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 DESIGN STYLE
|
||||||
|
|
||||||
|
- Clean, minimal, and soft dashboard
|
||||||
|
- Background: light gray (#f3f4f6)
|
||||||
|
- Card: white with subtle shadow
|
||||||
|
- Border radius: 16px–24px (rounded-2xl)
|
||||||
|
- Typography: medium contrast (not too bold)
|
||||||
|
- Primary color: navy blue (#1E3A5F)
|
||||||
|
- Accent: soft blue + neutral gray
|
||||||
|
- Icons inside circular solid background
|
||||||
|
|
||||||
|
Spacing:
|
||||||
|
- Use gap-6 consistently
|
||||||
|
- Internal padding: p-5 or p-6
|
||||||
|
- Layout must feel breathable (no clutter)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 LAYOUT STRUCTURE
|
||||||
|
|
||||||
|
### 🔹 TOP SECTION (4 STAT CARDS - GRID)
|
||||||
|
Grid: 4 columns (responsive → 2 / 1)
|
||||||
|
|
||||||
|
Each card contains:
|
||||||
|
- Title (small, muted)
|
||||||
|
- Big number (bold, large)
|
||||||
|
- Subtitle (small gray text)
|
||||||
|
- Right side: circular icon container
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Total Pengaduan → 42 → "Bulan ini"
|
||||||
|
- Baru → 14 → "Belum diproses"
|
||||||
|
- Diproses → 14 → "Sedang ditangani"
|
||||||
|
- Selesai → 14 → "Terselesaikan"
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- Mantine Card
|
||||||
|
- Group justify="space-between"
|
||||||
|
- Icon inside circle (bg navy, icon white)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 MAIN CHART (FULL WIDTH)
|
||||||
|
Title: "Tren Pengaduan"
|
||||||
|
|
||||||
|
- Use Recharts LineChart
|
||||||
|
- Smooth line (monotone)
|
||||||
|
- Show dots on each point
|
||||||
|
- Data: Apr → Okt
|
||||||
|
- Value range: 30–60
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Minimal grid (light dashed)
|
||||||
|
- No heavy colors (use gray/blue line)
|
||||||
|
- Rounded container card
|
||||||
|
- Add small top-right icon (expand)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 BOTTOM SECTION (3 COLUMN GRID)
|
||||||
|
|
||||||
|
### 🔹 LEFT: "Surat Terbanyak"
|
||||||
|
- Horizontal bar chart (Recharts)
|
||||||
|
- Categories:
|
||||||
|
- KTP
|
||||||
|
- KK
|
||||||
|
- Domisili
|
||||||
|
- Usaha
|
||||||
|
- Lainnya
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Dark blue bars
|
||||||
|
- Rounded edges
|
||||||
|
- Clean axis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 CENTER: "Pengajuan Terbaru"
|
||||||
|
List of activity cards:
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
- Name (bold)
|
||||||
|
- Subtitle (jenis surat)
|
||||||
|
- Time (small text)
|
||||||
|
- Status badge (kanan)
|
||||||
|
|
||||||
|
Status:
|
||||||
|
- baru → red
|
||||||
|
- proses → blue
|
||||||
|
- selesai → green
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Card per item
|
||||||
|
- Soft border
|
||||||
|
- Rounded
|
||||||
|
- Compact spacing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 RIGHT: "Ajuan Ide Inovatif"
|
||||||
|
List mirip dengan pengajuan terbaru:
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
- Nama
|
||||||
|
- Judul ide
|
||||||
|
- Waktu
|
||||||
|
- Button kecil "Detail"
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Right-aligned action button
|
||||||
|
- Light border
|
||||||
|
- Clean spacing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ COMPONENT STRUCTURE
|
||||||
|
|
||||||
|
components/
|
||||||
|
- StatCard.tsx
|
||||||
|
- LineChartCard.tsx
|
||||||
|
- BarChartCard.tsx
|
||||||
|
- ActivityList.tsx
|
||||||
|
- IdeaList.tsx
|
||||||
|
|
||||||
|
routes/
|
||||||
|
- dashboard.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ INTERACTIONS (IMPORTANT)
|
||||||
|
|
||||||
|
- Hover card → scale(1.02)
|
||||||
|
- Transition: 150ms ease
|
||||||
|
- Icon circle slightly pop on hover
|
||||||
|
- List item hover → subtle bg change
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 UX DETAILS
|
||||||
|
|
||||||
|
- Numbers must be visually dominant
|
||||||
|
- Icons must balance layout (not too big)
|
||||||
|
- Avoid heavy borders
|
||||||
|
- Keep everything aligned perfectly
|
||||||
|
- No clutter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 OUTPUT
|
||||||
|
|
||||||
|
- Modular React components (NOT one file)
|
||||||
|
- Clean code (production-ready)
|
||||||
|
- Use Mantine properly (no hacky inline styles unless needed)
|
||||||
|
- Use Tailwind only for layout/grid/spacing
|
||||||
BIN
Screenshot 2026-03-10 at 16.48.25.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts",
|
"dev": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"test": "bun test __tests__/api",
|
"test": "bun test __tests__/api",
|
||||||
"test:ui": "bun test --ui __tests__/api",
|
"test:ui": "bun test --ui __tests__/api",
|
||||||
"test:e2e": "bun run build && playwright test",
|
"test:e2e": "bun run build && playwright test",
|
||||||
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'",
|
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true",
|
||||||
"start": "NODE_ENV=production bun src/index.ts",
|
"start": "NODE_ENV=production bun src/index.ts",
|
||||||
"seed": "bun prisma/seed.ts"
|
"seed": "bun prisma/seed.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -133,7 +133,18 @@ async function main() {
|
|||||||
console.log("Database seeding completed.");
|
console.log("Database seeding completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
// Only auto-execute when run directly (not when imported)
|
||||||
console.error("Error during seeding:", error);
|
const isMainModule =
|
||||||
process.exit(1);
|
typeof require !== "undefined"
|
||||||
});
|
? require.main === module
|
||||||
|
: import.meta.path.endsWith("seed.ts");
|
||||||
|
|
||||||
|
if (isMainModule) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Error during seeding:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for programmatic use
|
||||||
|
export { seedAdminUser, seedDemoUsers, main as runSeed };
|
||||||
|
|||||||
BIN
public/SDGS-1.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/SDGS-16.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/SDGS-3.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/SDGS-7.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
public/light-mode.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/logo-desa-plus.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/white.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
58
scripts/build.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build script for production
|
||||||
|
* 1. Build CSS with PostCSS/Tailwind
|
||||||
|
* 2. Bundle JS with Bun (without CSS)
|
||||||
|
* 3. Replace CSS reference in HTML
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $ } from "bun";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import postcss from "postcss";
|
||||||
|
import tailwindcss from "@tailwindcss/postcss";
|
||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
|
||||||
|
console.log("🔨 Starting production build...");
|
||||||
|
|
||||||
|
// Ensure dist directory exists
|
||||||
|
if (!fs.existsSync("./dist")) {
|
||||||
|
fs.mkdirSync("./dist", { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Build CSS with PostCSS
|
||||||
|
console.log("🎨 Building CSS...");
|
||||||
|
const cssInput = fs.readFileSync("./src/index.css", "utf-8");
|
||||||
|
const cssResult = await postcss([tailwindcss(), autoprefixer()]).process(
|
||||||
|
cssInput,
|
||||||
|
{
|
||||||
|
from: "./src/index.css",
|
||||||
|
to: "./dist/index.css",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync("./dist/index.css", cssResult.css);
|
||||||
|
console.log("✅ CSS built successfully!");
|
||||||
|
|
||||||
|
// Step 2: Build JS with Bun (build HTML too, we'll fix CSS link later)
|
||||||
|
console.log("📦 Bundling JavaScript...");
|
||||||
|
await $`bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='"production"' --env='VITE_*'`;
|
||||||
|
|
||||||
|
// Step 3: Copy public assets
|
||||||
|
console.log("📁 Copying public assets...");
|
||||||
|
if (fs.existsSync("./public")) {
|
||||||
|
await $`cp -r public/* dist/ 2>/dev/null || true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Ensure HTML references the correct CSS
|
||||||
|
// Bun build might have renamed the CSS, we want to use our own index.css
|
||||||
|
console.log("🔧 Fixing HTML CSS reference...");
|
||||||
|
const htmlPath = "./dist/index.html";
|
||||||
|
if (fs.existsSync(htmlPath)) {
|
||||||
|
let html = fs.readFileSync(htmlPath, "utf-8");
|
||||||
|
// Replace any bundled CSS reference with our index.css
|
||||||
|
html = html.replace(/href="[^"]*\.css"/g, 'href="/index.css"');
|
||||||
|
fs.writeFileSync(htmlPath, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Build completed successfully!");
|
||||||
@@ -1,385 +1,38 @@
|
|||||||
import {
|
import { Grid, GridCol, Stack } from "@mantine/core";
|
||||||
Badge,
|
import { HeaderToggle } from "./umkm/header-toggle";
|
||||||
Button,
|
import { ProdukUnggulan } from "./umkm/produk-unggulan";
|
||||||
Card,
|
import type { SalesData } from "./umkm/sales-table";
|
||||||
Grid,
|
import { SalesTable } from "./umkm/sales-table";
|
||||||
GridCol,
|
import { SummaryCards } from "./umkm/summary-cards";
|
||||||
Group,
|
import { TopProducts } from "./umkm/top-products";
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconBuildingStore,
|
|
||||||
IconCategory,
|
|
||||||
IconCurrency,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const BumdesPage = () => {
|
const BumdesPage = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const handleDetailClick = (product: SalesData) => {
|
||||||
const dark = colorScheme === "dark";
|
console.log("Detail clicked for:", product);
|
||||||
|
// TODO: Open modal or navigate to detail page
|
||||||
const [timeFilter, setTimeFilter] = useState<string>("bulan");
|
};
|
||||||
|
|
||||||
// Sample data for KPI cards
|
|
||||||
const kpiData = [
|
|
||||||
{
|
|
||||||
title: "UMKM Aktif",
|
|
||||||
value: 45,
|
|
||||||
icon: <IconUsers size={24} />,
|
|
||||||
color: "darmasaba-blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "UMKM Terdaftar",
|
|
||||||
value: 68,
|
|
||||||
icon: <IconBuildingStore size={24} />,
|
|
||||||
color: "darmasaba-success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Omzet",
|
|
||||||
value: "Rp 48.000.000",
|
|
||||||
icon: <IconCurrency size={24} />,
|
|
||||||
color: "darmasaba-warning",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Kategori UMKM",
|
|
||||||
value: 34,
|
|
||||||
icon: <IconCategory size={24} />,
|
|
||||||
color: "darmasaba-danger",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for top products
|
|
||||||
const topProducts = [
|
|
||||||
{
|
|
||||||
rank: 1,
|
|
||||||
name: "Beras Premium Organik",
|
|
||||||
umkmOwner: "Warung Pak Joko",
|
|
||||||
growth: "+12%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 2,
|
|
||||||
name: "Keripik Singkong",
|
|
||||||
umkmOwner: "Ibu Sari Snack",
|
|
||||||
growth: "+8%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 3,
|
|
||||||
name: "Madu Alami",
|
|
||||||
umkmOwner: "Peternakan Lebah",
|
|
||||||
growth: "+5%",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for product sales
|
|
||||||
const productSales = [
|
|
||||||
{
|
|
||||||
produk: "Beras Premium Organik",
|
|
||||||
penjualanBulanIni: "Rp 8.500.000",
|
|
||||||
bulanLalu: "Rp 8.500.000",
|
|
||||||
trend: 10,
|
|
||||||
volume: "650 Kg",
|
|
||||||
stok: "850 Kg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Keripik Singkong",
|
|
||||||
penjualanBulanIni: "Rp 4.200.000",
|
|
||||||
bulanLalu: "Rp 3.800.000",
|
|
||||||
trend: 10,
|
|
||||||
volume: "320 Kg",
|
|
||||||
stok: "120 Kg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Madu Alami",
|
|
||||||
penjualanBulanIni: "Rp 3.750.000",
|
|
||||||
bulanLalu: "Rp 4.100.000",
|
|
||||||
trend: -8,
|
|
||||||
volume: "150 Liter",
|
|
||||||
stok: "45 Liter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Kecap Tradisional",
|
|
||||||
penjualanBulanIni: "Rp 2.800.000",
|
|
||||||
bulanLalu: "Rp 2.500.000",
|
|
||||||
trend: 12,
|
|
||||||
volume: "280 Botol",
|
|
||||||
stok: "95 Botol",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* KPI Cards */}
|
{/* KPI Summary Cards */}
|
||||||
<Grid gutter="md">
|
<SummaryCards />
|
||||||
{kpiData.map((kpi, index) => (
|
|
||||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{typeof kpi.value === "number"
|
|
||||||
? kpi.value.toLocaleString()
|
|
||||||
: kpi.value}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Badge variant="light" color={kpi.color} p={8} radius="md">
|
|
||||||
{kpi.icon}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Update Penjualan Produk Header */}
|
{/* Header with Time Range Toggle */}
|
||||||
<Card
|
<HeaderToggle />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center" px="md" py="xs">
|
|
||||||
<Title order={3} c={dark ? "dark.0" : "black"}>
|
|
||||||
Update Penjualan Produk
|
|
||||||
</Title>
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === "minggu" ? "filled" : "light"}
|
|
||||||
onClick={() => setTimeFilter("minggu")}
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Minggu ini
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === "bulan" ? "filled" : "light"}
|
|
||||||
onClick={() => setTimeFilter("bulan")}
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Bulan ini
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
{/* Main Content - 2 Column Layout */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{/* Produk Unggulan (Left Column) */}
|
{/* Left Panel - Produk Unggulan */}
|
||||||
<GridCol span={{ base: 12, lg: 4 }}>
|
<GridCol span={{ base: 12, lg: 4 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
|
<ProdukUnggulan />
|
||||||
<Card
|
<TopProducts />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Total Penjualan
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
Rp 28.500.000
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Produk Aktif
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
124 Produk
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Total Transaksi
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
1.240 Transaksi
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Top 3 Produk Terlaris */}
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Top 3 Produk Terlaris
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{topProducts.map((product) => (
|
|
||||||
<Group
|
|
||||||
key={product.rank}
|
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Badge
|
|
||||||
variant="filled"
|
|
||||||
color={
|
|
||||||
product.rank === 1
|
|
||||||
? "gold"
|
|
||||||
: product.rank === 2
|
|
||||||
? "gray"
|
|
||||||
: "bronze"
|
|
||||||
}
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{product.rank}
|
|
||||||
</Badge>
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.name}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{product.umkmOwner}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color={product.growth.startsWith("+") ? "green" : "red"}
|
|
||||||
>
|
|
||||||
{product.growth}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Detail Penjualan Produk (Right Column) */}
|
{/* Right Panel - Detail Penjualan Produk */}
|
||||||
<GridCol span={{ base: 12, lg: 8 }}>
|
<GridCol span={{ base: 12, lg: 8 }}>
|
||||||
<Card
|
<SalesTable onDetailClick={handleDetailClick} />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Title order={4} c={dark ? "dark.0" : "black"}>
|
|
||||||
Detail Penjualan Produk
|
|
||||||
</Title>
|
|
||||||
<Select
|
|
||||||
placeholder="Filter kategori"
|
|
||||||
data={[
|
|
||||||
{ value: "semua", label: "Semua Kategori" },
|
|
||||||
{ value: "makanan", label: "Makanan" },
|
|
||||||
{ value: "minuman", label: "Minuman" },
|
|
||||||
{ value: "kerajinan", label: "Kerajinan" },
|
|
||||||
]}
|
|
||||||
defaultValue="semua"
|
|
||||||
w={200}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Table striped highlightOnHover withColumnBorders>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Produk</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>
|
|
||||||
Penjualan Bulan Ini
|
|
||||||
</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Bulan Lalu</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Trend</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Volume</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Stok</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Aksi</Text>
|
|
||||||
</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{productSales.map((product, index) => (
|
|
||||||
<Table.Tr key={index}>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.produk}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.penjualanBulanIni}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>
|
|
||||||
{product.bulanLalu}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text c={product.trend >= 0 ? "green" : "red"}>
|
|
||||||
{product.trend >= 0 ? "↑" : "↓"}{" "}
|
|
||||||
{Math.abs(product.trend)}%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.volume}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color={
|
|
||||||
parseInt(product.stok) > 200 ? "green" : "yellow"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{product.stok}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
size="compact-sm"
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,510 +1,117 @@
|
|||||||
import {
|
import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
|
||||||
Calendar,
|
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
||||||
CheckCircle,
|
import { ActivityList } from "./dashboard/activity-list";
|
||||||
FileText,
|
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
||||||
MessageCircle,
|
import { ChartSurat } from "./dashboard/chart-surat";
|
||||||
Users,
|
import { DivisionProgress } from "./dashboard/division-progress";
|
||||||
} from "lucide-react";
|
import { SatisfactionChart } from "./dashboard/satisfaction-chart";
|
||||||
import {
|
import { SDGSCard } from "./dashboard/sdgs-card";
|
||||||
Bar,
|
import { StatCard } from "./dashboard/stat-card";
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
Cell,
|
|
||||||
Pie,
|
|
||||||
PieChart,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip, // Added Tooltip import
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
// Import Mantine components
|
const sdgsData = [
|
||||||
|
{
|
||||||
import {
|
title: "Desa Berenergi Bersih dan Terbarukan",
|
||||||
ActionIcon,
|
score: 99.64,
|
||||||
Badge,
|
image: "SDGS-7.png",
|
||||||
Box,
|
},
|
||||||
Card, // Added for icon containers
|
{
|
||||||
Grid,
|
title: "Desa Damai Berkeadilan",
|
||||||
Group,
|
score: 78.65,
|
||||||
Progress,
|
image: "SDGS-16.png",
|
||||||
Stack,
|
},
|
||||||
Text,
|
{
|
||||||
ThemeIcon,
|
title: "Desa Sehat dan Sejahtera",
|
||||||
Title,
|
score: 77.37,
|
||||||
useMantineColorScheme, // Add this import
|
image: "SDGS-3.png",
|
||||||
} from "@mantine/core";
|
},
|
||||||
|
{
|
||||||
const barChartData = [
|
title: "Desa Tanpa Kemiskinan",
|
||||||
{ month: "Jan", value: 145 },
|
score: 52.62,
|
||||||
{ month: "Feb", value: 165 },
|
image: "SDGS-1.png",
|
||||||
{ month: "Mar", value: 195 },
|
},
|
||||||
{ month: "Apr", value: 155 },
|
|
||||||
{ month: "Mei", value: 205 },
|
|
||||||
{ month: "Jun", value: 185 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const pieChartData = [
|
|
||||||
{ name: "Puas", value: 25 },
|
|
||||||
{ name: "Cukup", value: 25 },
|
|
||||||
{ name: "Kurang", value: 25 },
|
|
||||||
{ name: "Sangat puas", value: 25 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COLORS = ["#4E5BA6", "#F4C542", "#8CC63F", "#E57373"];
|
|
||||||
|
|
||||||
const divisiData = [
|
|
||||||
{ name: "Kesejahteraan", value: 37 },
|
|
||||||
{ name: "Pemerintahan", value: 26 },
|
|
||||||
{ name: "Keuangan", value: 17 },
|
|
||||||
{ name: "Sekretaris Desa", value: 15 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const eventData = [
|
|
||||||
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
|
|
||||||
{ date: "15 Oktober 2025", title: "Davest" },
|
|
||||||
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const apbdesData = [
|
|
||||||
{ name: "Belanja", value: 70, color: "blue" },
|
|
||||||
{ name: "Pendapatan", value: 90, color: "green" },
|
|
||||||
{ name: "Pembangunan", value: 50, color: "orange" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DashboardContent() {
|
export function DashboardContent() {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Stats Cards */}
|
{/* Header Metrics - 4 Stat Cards */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||||
<Card
|
<StatCard
|
||||||
p="md"
|
title="Surat Minggu Ini"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
value={99}
|
||||||
radius="md"
|
detail="14 baru, 14 diproses"
|
||||||
h="100%"
|
trend="12% dari minggu lalu ↗ +12%"
|
||||||
withBorder
|
trendValue={12}
|
||||||
bg={dark ? "#141D34" : "white"}
|
icon={<FileText style={{ width: "70%", height: "70%" }} />}
|
||||||
>
|
/>
|
||||||
<Group justify="space-between" align="flex-start" w="100%">
|
|
||||||
<Box style={{ flex: 1 }}>
|
|
||||||
<Text size="sm" c="dimmed" mb="xs">
|
|
||||||
Surat Minggu Ini
|
|
||||||
</Text>
|
|
||||||
<Group align="baseline" gap="xs">
|
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
99
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" mt="xs">
|
|
||||||
14 baru, 14 diproses
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="red" mt="xs">
|
|
||||||
12% dari minggu lalu ↗ +12%
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
color={dark ? "gray" : "darmasaba-blue"}
|
|
||||||
>
|
|
||||||
<FileText style={{ width: "70%", height: "70%" }} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||||
<Card
|
<StatCard
|
||||||
p="md"
|
title="Pengaduan Aktif"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
value={28}
|
||||||
radius="md"
|
detail="14 baru, 14 diproses"
|
||||||
h="100%"
|
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
|
||||||
withBorder
|
/>
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="flex-start" w="100%">
|
|
||||||
<Box style={{ flex: 1 }}>
|
|
||||||
<Text size="sm" c="dimmed" mb="xs">
|
|
||||||
Pengaduan Aktif
|
|
||||||
</Text>
|
|
||||||
<Group align="baseline" gap="xs">
|
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
28
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" mt="xs">
|
|
||||||
14 baru, 14 diproses
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
color={dark ? "gray" : "darmasaba-blue"}
|
|
||||||
>
|
|
||||||
<MessageCircle style={{ width: "70%", height: "70%" }} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||||
<Card
|
<StatCard
|
||||||
p="md"
|
title="Layanan Selesai"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
value={156}
|
||||||
radius="md"
|
detail="bulan ini"
|
||||||
h="100%"
|
trend="+8%"
|
||||||
withBorder
|
trendValue={8}
|
||||||
bg={dark ? "#141D34" : "white"}
|
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
|
||||||
>
|
/>
|
||||||
<Group justify="space-between" align="flex-start" w="100%">
|
|
||||||
<Box style={{ flex: 1 }}>
|
|
||||||
<Text size="sm" c="dimmed" mb="xs">
|
|
||||||
Layanan Selesai
|
|
||||||
</Text>
|
|
||||||
<Group align="baseline" gap="xs">
|
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
156
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" mt="xs">
|
|
||||||
bulan ini
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="red" mt="xs">
|
|
||||||
+8%
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
color={dark ? "gray" : "darmasaba-blue"}
|
|
||||||
>
|
|
||||||
<CheckCircle style={{ width: "70%", height: "70%" }} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||||
<Card
|
<StatCard
|
||||||
p="md"
|
title="Kepuasan Warga"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
value="87.2%"
|
||||||
radius="md"
|
detail="dari 482 responden"
|
||||||
h="100%"
|
icon={<Users style={{ width: "70%", height: "70%" }} />}
|
||||||
withBorder
|
/>
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="flex-start" w="100%">
|
|
||||||
<Box style={{ flex: 1 }}>
|
|
||||||
<Text size="sm" c="dimmed" mb="xs">
|
|
||||||
Kepuasan Warga
|
|
||||||
</Text>
|
|
||||||
<Group align="baseline" gap="xs">
|
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
87.2%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" mt="xs">
|
|
||||||
dari 482 responden
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
color={dark ? "gray" : "darmasaba-blue"}
|
|
||||||
>
|
|
||||||
<Users style={{ width: "70%", height: "70%" }} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Section 2: Chart & Division Progress */}
|
||||||
<Grid gutter="lg">
|
<Grid gutter="lg">
|
||||||
{/* Bar Chart */}
|
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<ChartSurat />
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Box>
|
|
||||||
<Title order={4} mb={5}>
|
|
||||||
Statistik Pengajuan Surat
|
|
||||||
</Title>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Trend pengajuan surat 6 bulan terakhir
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<ActionIcon variant="subtle" size="lg" radius="md">
|
|
||||||
{/* Original SVG converted to a generic Icon placeholder */}
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8 5L13 10L8 15"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<BarChart data={barChartData}>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
vertical={false}
|
|
||||||
stroke="var(--mantine-color-gray-3)"
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="month"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "var(--mantine-color-text)" }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
ticks={[0, 55, 110, 165, 220]}
|
|
||||||
tick={{ fill: "var(--mantine-color-text)" }}
|
|
||||||
/>
|
|
||||||
<Tooltip />
|
|
||||||
<Bar
|
|
||||||
dataKey="value"
|
|
||||||
fill="var(--mantine-color-blue-filled)"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||||
{/* Pie Chart */}
|
<SatisfactionChart />
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Title order={4} mb={5}>
|
|
||||||
Tingkat Kepuasan
|
|
||||||
</Title>
|
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
|
||||||
Tingkat kepuasan layanan
|
|
||||||
</Text>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={pieChartData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={80}
|
|
||||||
outerRadius={120}
|
|
||||||
paddingAngle={2}
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{pieChartData.map((_entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
<Group justify="center" gap="md" mt="md">
|
|
||||||
<Group gap="xs">
|
|
||||||
<Box
|
|
||||||
w={12}
|
|
||||||
h={12}
|
|
||||||
style={{ backgroundColor: COLORS[0], borderRadius: "50%" }}
|
|
||||||
/>
|
|
||||||
<Text size="sm">Sangat puas (0%)</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Box
|
|
||||||
w={12}
|
|
||||||
h={12}
|
|
||||||
style={{ backgroundColor: COLORS[1], borderRadius: "50%" }}
|
|
||||||
/>
|
|
||||||
<Text size="sm">Puas (0%)</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Box
|
|
||||||
w={12}
|
|
||||||
h={12}
|
|
||||||
style={{ backgroundColor: COLORS[2], borderRadius: "50%" }}
|
|
||||||
/>
|
|
||||||
<Text size="sm">Cukup (0%)</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Box
|
|
||||||
w={12}
|
|
||||||
h={12}
|
|
||||||
style={{ backgroundColor: COLORS[3], borderRadius: "50%" }}
|
|
||||||
/>
|
|
||||||
<Text size="sm">Kurang (0%)</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Bottom Section */}
|
{/* Section 3: APBDes Chart */}
|
||||||
<Grid gutter="lg">
|
<Grid gutter="lg">
|
||||||
{/* Divisi Teraktif */}
|
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<DivisionProgress />
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Group gap="xs" mb="lg">
|
|
||||||
<Box>
|
|
||||||
{/* Original SVG icon */}
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
x="3"
|
|
||||||
y="3"
|
|
||||||
width="7"
|
|
||||||
height="7"
|
|
||||||
rx="1"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="3"
|
|
||||||
y="14"
|
|
||||||
width="7"
|
|
||||||
height="7"
|
|
||||||
rx="1"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="14"
|
|
||||||
y="3"
|
|
||||||
width="7"
|
|
||||||
height="7"
|
|
||||||
rx="1"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="14"
|
|
||||||
y="14"
|
|
||||||
width="7"
|
|
||||||
height="7"
|
|
||||||
rx="1"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Box>
|
|
||||||
<Title order={4}>Divisi Teraktif</Title>
|
|
||||||
</Group>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{divisiData.map((divisi, index) => (
|
|
||||||
<Box key={index}>
|
|
||||||
<Group justify="space-between" mb={5}>
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{divisi.name}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={600}>
|
|
||||||
{divisi.value} Kegiatan
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Progress
|
|
||||||
value={(divisi.value / 37) * 100}
|
|
||||||
size="sm"
|
|
||||||
radius="xl"
|
|
||||||
color="blue"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||||
{/* Kalender */}
|
<ActivityList />
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
{/* <SatisfactionChart /> */}
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
>
|
|
||||||
<Group gap="xs" mb="lg">
|
|
||||||
<Calendar style={{ width: 20, height: 20 }} />
|
|
||||||
<Title order={4}>Kalender & Kegiatan Mendatang</Title>
|
|
||||||
</Group>
|
|
||||||
<Stack gap="md">
|
|
||||||
{eventData.map((event, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
|
||||||
paddingLeft: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{event.date}
|
|
||||||
</Text>
|
|
||||||
<Text fw={500}>{event.title}</Text>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* APBDes Chart */}
|
<ChartAPBDes />
|
||||||
<Card
|
|
||||||
p="md"
|
{/* Section 6: SDGs Desa Cards */}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
<Grid gutter="md">
|
||||||
radius="md"
|
{sdgsData.map((sdg, index) => (
|
||||||
withBorder
|
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
|
||||||
bg={dark ? "#141D34" : "white"}
|
<SDGSCard
|
||||||
>
|
image={<Image src={sdg.image} alt={sdg.title} />}
|
||||||
<Title order={4} mb="lg">
|
title={sdg.title}
|
||||||
Grafik APBDes
|
score={sdg.score}
|
||||||
</Title>
|
/>
|
||||||
<Stack gap="xs">
|
</Grid.Col>
|
||||||
{apbdesData.map((data, index) => (
|
))}
|
||||||
<Grid key={index} align="center">
|
</Grid>
|
||||||
<Grid.Col span={3}>
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{data.name}
|
|
||||||
</Text>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={9}>
|
|
||||||
<Progress
|
|
||||||
value={data.value}
|
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
color={data.color}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/components/dashboard/activity-list.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
|
interface EventData {
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events: EventData[] = [
|
||||||
|
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
|
||||||
|
{ date: "15 Oktober 2025", title: "Davest" },
|
||||||
|
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ActivityList() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="lg">
|
||||||
|
<Calendar
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
color={dark ? "#E2E8F0" : "#1E3A5F"}
|
||||||
|
/>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Kalender & Kegiatan Mendatang
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="md">
|
||||||
|
{events.map((event, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
||||||
|
paddingLeft: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{event.date}
|
||||||
|
</Text>
|
||||||
|
<Text fw={500} c={dark ? "white" : "gray.9"}>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/components/dashboard/chart-apbdes.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
Cell,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
const apbdesData = [
|
||||||
|
{ name: "Belanja", value: 70, color: "#3B82F6" },
|
||||||
|
{ name: "Pangan", value: 45, color: "#22C55E" },
|
||||||
|
{ name: "Pembiayaan", value: 55, color: "#FACC15" },
|
||||||
|
{ name: "Pendapatan", value: 90, color: "#3B82F6" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ChartAPBDes() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||||
|
Grafik APBDes
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{apbdesData.map((item, index) => (
|
||||||
|
<Group key={index} align="center" gap="md">
|
||||||
|
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<ResponsiveContainer width="100%" height={20}>
|
||||||
|
<BarChart layout="vertical" data={[item]}>
|
||||||
|
<XAxis type="number" hide domain={[0, 100]} />
|
||||||
|
<YAxis type="category" hide dataKey="name" />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) => [`${value}%`, ""]}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
|
||||||
|
<Cell fill={item.color} />
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/components/dashboard/chart-surat.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ month: "Jan", value: 150 },
|
||||||
|
{ month: "Feb", value: 165 },
|
||||||
|
{ month: "Mar", value: 195 },
|
||||||
|
{ month: "Apr", value: 160 },
|
||||||
|
{ month: "Mei", value: 205 },
|
||||||
|
{ month: "Jun", value: 185 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ChartSurat() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Box>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||||
|
Statistik Pengajuan Surat
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Trend pengajuan surat 6 bulan terakhir
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<ActionIcon variant="subtle" size="lg" radius="md">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 5L13 10L8 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={chartData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
ticks={[0, 55, 110, 165, 220]}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="var(--mantine-color-blue-filled)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/dashboard/division-progress.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Progress,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface DivisionData {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const divisionData: DivisionData[] = [
|
||||||
|
{ name: "Kesejahteraan", value: 37 },
|
||||||
|
{ name: "Pemberdayaan", value: 26 },
|
||||||
|
{ name: "Keuangan", value: 17 },
|
||||||
|
{ name: "Sekretaris Desa", value: 15 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const max_value = 37;
|
||||||
|
|
||||||
|
export function DivisionProgress() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||||
|
Divisi Teraktif
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{divisionData.map((divisi, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<Group justify="space-between" mb={5}>
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
|
||||||
|
{divisi.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
|
{divisi.value} Kegiatan
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={(divisi.value / max_value) * 100}
|
||||||
|
size="sm"
|
||||||
|
radius="xl"
|
||||||
|
color="blue"
|
||||||
|
animated
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { ActivityList } from "./activity-list";
|
||||||
|
export { ChartAPBDes } from "./chart-apbdes";
|
||||||
|
export { ChartSurat } from "./chart-surat";
|
||||||
|
export { DivisionProgress } from "./division-progress";
|
||||||
|
export { SatisfactionChart } from "./satisfaction-chart";
|
||||||
|
export { SDGSCard } from "./sdgs-card";
|
||||||
|
export { StatCard } from "./stat-card";
|
||||||
83
src/components/dashboard/satisfaction-chart.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||||
|
|
||||||
|
const satisfactionData = [
|
||||||
|
{ name: "Sangat Puas", value: 25, color: "#4E5BA6" },
|
||||||
|
{ name: "Puas", value: 25, color: "#F4C542" },
|
||||||
|
{ name: "Cukup", value: 25, color: "#8CC63F" },
|
||||||
|
{ name: "Kurang", value: 25, color: "#E57373" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SatisfactionChart() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||||
|
Tingkat Kepuasan
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Tingkat kepuasan layanan
|
||||||
|
</Text>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={satisfactionData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={80}
|
||||||
|
outerRadius={120}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{satisfactionData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Group justify="center" gap="md" mt="md">
|
||||||
|
{satisfactionData.map((item, index) => (
|
||||||
|
<Group key={index} gap="xs">
|
||||||
|
<Box
|
||||||
|
w={12}
|
||||||
|
h={12}
|
||||||
|
style={{ backgroundColor: item.color, borderRadius: "50%" }}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/components/dashboard/sdgs-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Box, Card, Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface SDGSCardProps {
|
||||||
|
title: string;
|
||||||
|
score: number;
|
||||||
|
image: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SDGSCard({ title, score, image }: SDGSCardProps) {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
|
<Box>{image}</Box>
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
ta={"center"}
|
||||||
|
size="sm"
|
||||||
|
c={dark ? "white" : "gray.8"}
|
||||||
|
fw={500}
|
||||||
|
mb="xs"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text ta={"center"} size="xl" c={dark ? "white" : "gray.8"} fw={700}>
|
||||||
|
{score.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
src/components/dashboard/stat-card.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface StatCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
detail?: string;
|
||||||
|
trend?: string;
|
||||||
|
trendValue?: number;
|
||||||
|
icon: ReactNode;
|
||||||
|
iconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
detail,
|
||||||
|
trend,
|
||||||
|
trendValue,
|
||||||
|
icon,
|
||||||
|
iconColor = "#1E3A5F",
|
||||||
|
}: StatCardProps) {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const isPositiveTrend = trendValue ? trendValue >= 0 : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<Text size="sm" c="dimmed" mb="xs">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Group align="baseline" gap="xs">
|
||||||
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{detail && (
|
||||||
|
<Text size="sm" c="dimmed" mt="xs">
|
||||||
|
{detail}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{trend && (
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
c={isPositiveTrend ? "green" : "red"}
|
||||||
|
mt="xs"
|
||||||
|
fw={500}
|
||||||
|
>
|
||||||
|
{trend}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="filled"
|
||||||
|
size="xl"
|
||||||
|
radius="xl"
|
||||||
|
color={dark ? "gray" : iconColor}
|
||||||
|
bg={dark ? "gray" : iconColor}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,116 +1,73 @@
|
|||||||
import { BarChart, PieChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconArrowDown,
|
Baby,
|
||||||
IconArrowUp,
|
BarChart3,
|
||||||
IconBabyCarriage,
|
Building2,
|
||||||
IconSkull,
|
Home,
|
||||||
} from "@tabler/icons-react";
|
PieChart as PieChartIcon,
|
||||||
import React from "react";
|
TrendingDown,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Total Penduduk",
|
title: "Total Penduduk",
|
||||||
value: "5.634",
|
value: "5.634",
|
||||||
sub: "Aktif terdaftar",
|
subtitle: "Aktif terdaftar",
|
||||||
icon: (
|
icon: Users,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon penduduk"
|
|
||||||
>
|
|
||||||
<title>Total Penduduk</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Kepala Keluarga",
|
title: "Kepala Keluarga",
|
||||||
value: "1.354",
|
value: "1.354",
|
||||||
sub: "Total KK",
|
subtitle: "Total KK",
|
||||||
icon: (
|
icon: Home,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kepala keluarga"
|
|
||||||
>
|
|
||||||
<title>Kepala Keluarga</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Kelahiran",
|
title: "Kelahiran",
|
||||||
value: "23",
|
value: "23",
|
||||||
sub: "Tahun ini",
|
subtitle: "Tahun ini",
|
||||||
icon: (
|
icon: Baby,
|
||||||
<IconBabyCarriage
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kelahiran"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Kemiskinan",
|
title: "Kemiskinan",
|
||||||
value: "324",
|
value: "324",
|
||||||
delta: "-10% dari tahun lalu",
|
subtitle: "-10% dari tahun lalu",
|
||||||
deltaType: "positive",
|
trend: "positive",
|
||||||
icon: (
|
icon: TrendingDown,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kemiskinan"
|
|
||||||
>
|
|
||||||
<title>Kemiskinan</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Age Distribution Data
|
||||||
const ageDistributionData = [
|
const ageDistributionData = [
|
||||||
{ ageRange: "17-25", total: 850 },
|
{ ageRange: "17-25", total: 850 },
|
||||||
{ ageRange: "26-35", total: 1200 },
|
{ ageRange: "26-35", total: 1200 },
|
||||||
@@ -120,6 +77,7 @@ const ageDistributionData = [
|
|||||||
{ ageRange: "65+", total: 484 },
|
{ ageRange: "65+", total: 484 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Job Distribution Data
|
||||||
const jobDistributionData = [
|
const jobDistributionData = [
|
||||||
{ job: "Sipil", total: 1200 },
|
{ job: "Sipil", total: 1200 },
|
||||||
{ job: "Guru", total: 850 },
|
{ job: "Guru", total: 850 },
|
||||||
@@ -128,283 +86,597 @@ const jobDistributionData = [
|
|||||||
{ job: "Wiraswasta", total: 984 },
|
{ job: "Wiraswasta", total: 984 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Religion Data
|
||||||
const religionData = [
|
const religionData = [
|
||||||
{ religion: "Hindu", total: 4234, color: "red" },
|
{ name: "Hindu", value: 4234, color: "#EF4444" },
|
||||||
{ religion: "Islam", total: 856, color: "blue" },
|
{ name: "Islam", value: 856, color: "#3B82F6" },
|
||||||
{ religion: "Kristen", total: 412, color: "green" },
|
{ name: "Kristen", value: 412, color: "#22C55E" },
|
||||||
{ religion: "Buddha", total: 202, color: "yellow" },
|
{ name: "Buddha", value: 202, color: "#FACC15" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Banjar Data
|
||||||
const banjarData = [
|
const banjarData = [
|
||||||
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
|
{ banjar: "Darmasaba", population: 1200, kk: 300, poor: 45 },
|
||||||
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
|
{ banjar: "Manesa", population: 950, kk: 240, poor: 32 },
|
||||||
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
|
{ banjar: "Cabe", population: 800, kk: 200, poor: 28 },
|
||||||
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
|
{ banjar: "Penenjoan", population: 1100, kk: 280, poor: 38 },
|
||||||
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
|
{ banjar: "Baler Pasar", population: 984, kk: 250, poor: 42 },
|
||||||
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
|
{ banjar: "Bucu", population: 600, kk: 184, poor: 25 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Dynamic Stats Data
|
||||||
const dynamicStats = [
|
const dynamicStats = [
|
||||||
{
|
{
|
||||||
title: "Kelahiran",
|
title: "Kelahiran",
|
||||||
value: "23",
|
value: "23",
|
||||||
icon: <IconBabyCarriage size={16} />,
|
icon: Baby,
|
||||||
color: "green",
|
color: "#22C55E",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Kematian",
|
title: "Kematian",
|
||||||
value: "12",
|
value: "12",
|
||||||
icon: <IconSkull size={16} />,
|
icon: TrendingDown,
|
||||||
color: "red",
|
color: "#EF4444",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pindah Masuk",
|
title: "Pindah Masuk",
|
||||||
value: "45",
|
value: "45",
|
||||||
icon: <IconArrowDown size={16} />,
|
icon: Users,
|
||||||
color: "blue",
|
color: "#3B82F6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pindah Keluar",
|
title: "Pindah Keluar",
|
||||||
value: "32",
|
value: "32",
|
||||||
icon: <IconArrowUp size={16} />,
|
icon: Users,
|
||||||
color: "orange",
|
color: "#3B82F6",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Sektor Unggulan Data
|
||||||
|
const sektorUnggulanData = [
|
||||||
|
{ sektor: "Pertanian", value: 65 },
|
||||||
|
{ sektor: "Perdagangan", value: 45 },
|
||||||
|
{ sektor: "Industri", value: 38 },
|
||||||
|
{ sektor: "Jasa", value: 52 },
|
||||||
|
];
|
||||||
|
|
||||||
const DemografiPekerjaan = () => {
|
const DemografiPekerjaan = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
return (
|
|
||||||
<Box className="space-y-6">
|
|
||||||
<Stack gap="xl">
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
{kpiData.map((kpi) => (
|
|
||||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="flex-start" mb="xs">
|
|
||||||
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6",
|
|
||||||
color: dark
|
|
||||||
? "var(--mantine-color-dark-3)"
|
|
||||||
: "var(--mantine-color-dimmed)",
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: dark
|
|
||||||
? "dark.3"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}>
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Charts Section */}
|
return (
|
||||||
<Grid gutter="lg">
|
<Stack gap="lg">
|
||||||
{/* Grafik Pengelompokan Umur */}
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((item) => (
|
||||||
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Grafik Pengelompokan Umur
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<BarChart
|
{item.title}
|
||||||
h={300}
|
</Text>
|
||||||
data={ageDistributionData}
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
dataKey="ageRange"
|
{item.value}
|
||||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
</Text>
|
||||||
withLegend
|
<Group gap={4} align="flex-start">
|
||||||
/>
|
{item.trend === "positive" && (
|
||||||
|
<TrendingDown size={14} color="#22C55E" />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c={
|
||||||
|
item.trend === "positive"
|
||||||
|
? "green"
|
||||||
|
: dark
|
||||||
|
? "gray.4"
|
||||||
|
: "gray.5"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Demografi Pekerjaan */}
|
{/* ROW 2 - 3 COLUMNS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="lg">
|
||||||
<Card
|
{/* LEFT: PENGELOMPOKAN UMUR */}
|
||||||
p="md"
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
radius="md"
|
<Card
|
||||||
withBorder
|
p="md"
|
||||||
bg={dark ? "#141D34" : "white"}
|
radius="xl"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
withBorder
|
||||||
>
|
bg={dark ? "#1E293B" : "white"}
|
||||||
<Title order={3} fw={500} mb="md">
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Pengelompokan Umur
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<BarChart data={ageDistributionData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="ageRange"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
maxBarSize={40}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* CENTER: DEMOGRAFI PEKERJAAN */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Building2 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Demografi Pekerjaan
|
Demografi Pekerjaan
|
||||||
</Title>
|
</Title>
|
||||||
<BarChart
|
</Group>
|
||||||
h={300}
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
data={jobDistributionData}
|
<BarChart data={jobDistributionData} layout="vertical">
|
||||||
dataKey="job"
|
<CartesianGrid
|
||||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
strokeDasharray="3 3"
|
||||||
withLegend
|
horizontal={false}
|
||||||
/>
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
</Card>
|
/>
|
||||||
</Grid.Col>
|
<XAxis
|
||||||
</Grid>
|
type="number"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="job"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
width={90}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={30}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Agama & Data per Banjar */}
|
{/* RIGHT: STATISTIK DINAMIKA PENDUDUK */}
|
||||||
<Grid gutter="lg">
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
{/* Distribusi Agama */}
|
<Card
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
p="md"
|
||||||
<Card
|
radius="xl"
|
||||||
p="md"
|
withBorder
|
||||||
radius="md"
|
bg={dark ? "#1E293B" : "white"}
|
||||||
withBorder
|
style={{
|
||||||
bg={dark ? "#141D34" : "white"}
|
borderColor: dark ? "#334155" : "white",
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
>
|
}}
|
||||||
<Title order={3} fw={500} mb="md">
|
h="100%"
|
||||||
Distribusi Agama
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Dinamika Penduduk
|
||||||
</Title>
|
</Title>
|
||||||
<PieChart
|
</Group>
|
||||||
h={300}
|
<Grid gutter="sm">
|
||||||
data={religionData.map((item) => ({
|
{dynamicStats.map((stat, index) => (
|
||||||
name: item.religion,
|
<Grid.Col key={index} span={6}>
|
||||||
value: item.total,
|
<Card
|
||||||
color: item.color,
|
p="sm"
|
||||||
}))}
|
radius="lg"
|
||||||
withLabels
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
withLabelsLine
|
style={{
|
||||||
labelsPosition="outside"
|
transition: "transform 0.15s ease",
|
||||||
labelsType="percent"
|
cursor: "pointer",
|
||||||
/>
|
}}
|
||||||
</Card>
|
>
|
||||||
</Grid.Col>
|
<Stack gap={2} align="center">
|
||||||
|
<ThemeIcon
|
||||||
{/* Data per Banjar */}
|
color={stat.color}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
variant="filled"
|
||||||
<Card
|
size="md"
|
||||||
p="md"
|
radius="lg"
|
||||||
radius="md"
|
>
|
||||||
withBorder
|
<stat.icon size={14} />
|
||||||
bg={dark ? "#141D34" : "white"}
|
</ThemeIcon>
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
>
|
|
||||||
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
|
|
||||||
Data per Banjar
|
|
||||||
</Title>
|
|
||||||
<Table striped highlightOnHover>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Banjar</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Penduduk</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>KK</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Miskin</Text>
|
|
||||||
</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{banjarData.map((item, index) => (
|
|
||||||
<Table.Tr key={`${item.banjar}-${index}`}>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>{item.banjar}</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.population.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.kk.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "red.4" : "red"}>
|
|
||||||
{item.poor.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Statistik Dinamika Penduduk */}
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
|
|
||||||
Statistik Dinamika Penduduk
|
|
||||||
</Title>
|
|
||||||
<Grid gutter="md">
|
|
||||||
{dynamicStats.map((stat, index) => (
|
|
||||||
<Grid.Col
|
|
||||||
key={`${stat.title}-${index}`}
|
|
||||||
span={{ base: 12, md: 3 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Box>
|
|
||||||
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{stat.title}
|
{stat.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Title order={4} fw={700} c={stat.color}>
|
<Text
|
||||||
|
size="lg"
|
||||||
|
fw={700}
|
||||||
|
c={stat.color}
|
||||||
|
style={{ lineHeight: 1 }}
|
||||||
|
>
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</Title>
|
</Text>
|
||||||
</Box>
|
</Stack>
|
||||||
<Box c={stat.color}>{stat.icon}</Box>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ROW 3 - 3 COLUMNS */}
|
||||||
|
<Grid gutter="lg">
|
||||||
|
{/* LEFT: DISTRIBUSI AGAMA */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Distribusi Agama
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={religionData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={90}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{religionData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Stack gap="xs" mt="md">
|
||||||
|
{religionData.map((item, index) => (
|
||||||
|
<Group key={index} justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Box
|
||||||
|
w={10}
|
||||||
|
h={10}
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
</Grid.Col>
|
{item.value.toLocaleString()}
|
||||||
))}
|
</Text>
|
||||||
</Grid>
|
</Group>
|
||||||
</Card>
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* CENTER: DATA PER BANJAR */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Users size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Data per Banjar
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Banjar
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Penduduk
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KK
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Miskin
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{banjarData.map((item, index) => (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
index % 2 === 0
|
||||||
|
? dark
|
||||||
|
? "#334155"
|
||||||
|
: "#F8FAFC"
|
||||||
|
: "transparent",
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.banjar}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.population.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.kk.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#EF4444",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.poor.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: STATISTIK SEKTOR UNGGULAN */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Sektor Unggulan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<BarChart data={sektorUnggulanData} layout="vertical">
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
horizontal={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
type="number"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="sektor"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
width={90}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={40}
|
||||||
|
>
|
||||||
|
{sektorUnggulanData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill="#1E3A5F" />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,143 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useLocation } from "@tanstack/react-router";
|
import {
|
||||||
import { Bell, Moon, Sun } from "lucide-react";
|
IconLayoutSidebarLeftCollapse,
|
||||||
|
IconUserShield,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react";
|
||||||
|
|
||||||
export function Header() {
|
interface HeaderProps {
|
||||||
|
onSidebarToggle?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ onSidebarToggle }: HeaderProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const title =
|
// Define page titles based on route
|
||||||
location.pathname === "/"
|
const getPageTitle = () => {
|
||||||
? "Desa Darmasaba"
|
switch (location.pathname) {
|
||||||
: "Desa Darmasaba";
|
case "/":
|
||||||
|
return "Beranda";
|
||||||
|
case "/kinerja-divisi":
|
||||||
|
return "Kinerja Divisi";
|
||||||
|
case "/pengaduan-layanan-publik":
|
||||||
|
return "Pengaduan & Layanan Publik";
|
||||||
|
case "/jenna-analytic":
|
||||||
|
return "Jenna Analytic";
|
||||||
|
case "/demografi-pekerjaan":
|
||||||
|
return "Demografi & Kependudukan";
|
||||||
|
case "/keuangan-anggaran":
|
||||||
|
return "Keuangan & Anggaran";
|
||||||
|
case "/bumdes":
|
||||||
|
return "Bumdes & UMKM Desa";
|
||||||
|
case "/sosial":
|
||||||
|
return "Sosial";
|
||||||
|
case "/keamanan":
|
||||||
|
return "Keamanan";
|
||||||
|
case "/bantuan":
|
||||||
|
return "Bantuan";
|
||||||
|
case "/pengaturan":
|
||||||
|
case "/pengaturan/umum":
|
||||||
|
case "/pengaturan/notifikasi":
|
||||||
|
case "/pengaturan/keamanan":
|
||||||
|
case "/pengaturan/akses-dan-tim":
|
||||||
|
return "Pengaturan";
|
||||||
|
default:
|
||||||
|
return "Desa Darmasaba";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Group justify="space-between" w="100%">
|
||||||
style={{
|
{/* Title */}
|
||||||
display: "grid",
|
<Group gap="md">
|
||||||
gridTemplateColumns: "1fr auto 1fr",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* LEFT SPACER (burger sudah di luar) */}
|
|
||||||
<Box />
|
|
||||||
|
|
||||||
{/* CENTER TITLE */}
|
|
||||||
<Text
|
|
||||||
c="white"
|
|
||||||
fw={600}
|
|
||||||
size="md"
|
|
||||||
style={{
|
|
||||||
textAlign: "center",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* RIGHT ICONS */}
|
|
||||||
<Group gap="xs" justify="flex-end">
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={toggleColorScheme}
|
onClick={onSidebarToggle}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
size="lg"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
visibleFrom="sm"
|
||||||
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
{dark ? <Sun size={18} /> : <Moon size={18} />}
|
<IconLayoutSidebarLeftCollapse
|
||||||
</ActionIcon>
|
color="white"
|
||||||
|
style={{ width: "70%", height: "70%" }}
|
||||||
<ActionIcon variant="subtle" radius="xl" pos="relative">
|
/>
|
||||||
<Bell size={18} />
|
|
||||||
<Badge
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
style={{ position: "absolute", top: -4, right: -4 }}
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</Badge>
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
{/* <Title order={3} c={"white"}>
|
||||||
|
{getPageTitle()}
|
||||||
|
</Title> */}
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
|
||||||
|
{/* Right Section */}
|
||||||
|
<Group gap="md">
|
||||||
|
{/* User Info */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Box ta="right">
|
||||||
|
<Text c={"white"} size="sm" fw={500}>
|
||||||
|
I. B. Surya Prabhawa M...
|
||||||
|
</Text>
|
||||||
|
<Text c={"white"} size="xs">
|
||||||
|
Kepala Desa
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Avatar color="blue" radius="xl">
|
||||||
|
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
|
||||||
|
</Avatar>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<Divider orientation="vertical" h={30} />
|
||||||
|
|
||||||
|
{/* Icons */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => toggleColorScheme()}
|
||||||
|
variant="subtle"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
aria-label="Toggle color scheme"
|
||||||
|
>
|
||||||
|
{dark ? (
|
||||||
|
<Sun color="white" style={{ width: "70%", height: "70%" }} />
|
||||||
|
) : (
|
||||||
|
<Moon color="white" style={{ width: "70%", height: "70%" }} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
|
||||||
|
<Bell color="white" style={{ width: "70%", height: "70%" }} />
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
color="red"
|
||||||
|
variant="filled"
|
||||||
|
style={{ position: "absolute", top: 0, right: 0 }}
|
||||||
|
radius={"xl"}
|
||||||
|
>
|
||||||
|
10
|
||||||
|
</Badge>
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon variant="subtle" size="lg" radius="xl">
|
||||||
|
<IconUserShield
|
||||||
|
color="white"
|
||||||
|
style={{ width: "70%", height: "70%" }}
|
||||||
|
onClick={() => navigate({ to: "/signin" })}
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ const HelpPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
|
<Title order={1} mb="xl" ta="center">
|
||||||
|
Pusat Bantuan
|
||||||
|
</Title>
|
||||||
|
<Text size="lg" color="dimmed" ta="center" mb="xl">
|
||||||
|
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Statistics Section */}
|
{/* Statistics Section */}
|
||||||
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
@@ -174,7 +181,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconBook size={24} />}
|
icon={<IconBook size={24} color="white" />}
|
||||||
title="Panduan Memulai"
|
title="Panduan Memulai"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -204,7 +211,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconVideo size={24} />}
|
icon={<IconVideo size={24} color="white" />}
|
||||||
title="Video Tutorial"
|
title="Video Tutorial"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -234,7 +241,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconHelpCircle size={24} />}
|
icon={<IconHelpCircle size={24} color="white" />}
|
||||||
title="FAQ"
|
title="FAQ"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -266,7 +273,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconHeadphones size={24} />}
|
icon={<IconHeadphones size={24} color="white" />}
|
||||||
title="Hubungi Support"
|
title="Hubungi Support"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -301,7 +308,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconFileText size={24} />}
|
icon={<IconFileText size={24} color="white" />}
|
||||||
title="Dokumentasi"
|
title="Dokumentasi"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -333,7 +340,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconMessage size={24} />}
|
icon={<IconMessage size={24} color="white" />}
|
||||||
title="Jenna - Virtual Assistant"
|
title="Jenna - Virtual Assistant"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,123 +1,79 @@
|
|||||||
import { BarChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Progress,
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import React from "react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
MessageCircle,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Interaksi Hari Ini",
|
title: "Interaksi Hari Ini",
|
||||||
value: "61",
|
value: "61",
|
||||||
delta: "+15% dari kemarin",
|
subtitle: "+15% dari kemarin",
|
||||||
deltaType: "positive",
|
trend: "positive",
|
||||||
icon: (
|
icon: MessageCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Jawaban Otomatis",
|
title: "Jawaban Otomatis",
|
||||||
value: "87%",
|
value: "87%",
|
||||||
sub: "53 dari 61 interaksi",
|
subtitle: "53 dari 61 interaksi",
|
||||||
icon: (
|
icon: CheckCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Belum Ditindak",
|
title: "Belum Ditindak",
|
||||||
value: "8",
|
value: "8",
|
||||||
sub: "Perlu respon manual",
|
subtitle: "Perlu respon manual",
|
||||||
deltaType: "negative",
|
icon: AlertTriangle,
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Waktu Respon",
|
title: "Waktu Respon",
|
||||||
value: "2.3 sec",
|
value: "2.3 sec",
|
||||||
sub: "Rata-rata",
|
subtitle: "Rata-rata",
|
||||||
icon: (
|
icon: Clock,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Chart Data
|
||||||
const chartData = [
|
const chartData = [
|
||||||
{ day: "Sen", total: 100 },
|
{ day: "Sen", total: 45 },
|
||||||
{ day: "Sel", total: 120 },
|
{ day: "Sel", total: 62 },
|
||||||
{ day: "Rab", total: 90 },
|
{ day: "Rab", total: 38 },
|
||||||
{ day: "Kam", total: 150 },
|
{ day: "Kam", total: 75 },
|
||||||
{ day: "Jum", total: 110 },
|
{ day: "Jum", total: 58 },
|
||||||
{ day: "Sab", total: 80 },
|
{ day: "Sab", total: 32 },
|
||||||
{ day: "Min", total: 130 },
|
{ day: "Min", total: 51 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Top Topics Data
|
||||||
const topTopics = [
|
const topTopics = [
|
||||||
{ topic: "Cara mengurus KTP", count: 89 },
|
{ topic: "Cara mengurus KTP", count: 89 },
|
||||||
{ topic: "Syarat Kartu Keluarga", count: 76 },
|
{ topic: "Syarat Kartu Keluarga", count: 76 },
|
||||||
@@ -126,6 +82,7 @@ const topTopics = [
|
|||||||
{ topic: "Info program bansos", count: 48 },
|
{ topic: "Info program bansos", count: 48 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Busy Hours Data
|
||||||
const busyHours = [
|
const busyHours = [
|
||||||
{ period: "Pagi (08–12)", percentage: 30 },
|
{ period: "Pagi (08–12)", percentage: 30 },
|
||||||
{ period: "Siang (12–16)", percentage: 40 },
|
{ period: "Siang (12–16)", percentage: 40 },
|
||||||
@@ -138,146 +95,206 @@ const JennaAnalytic = () => {
|
|||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="space-y-6">
|
<Stack gap="lg">
|
||||||
<Stack gap="xl">
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
{/* KPI Cards */}
|
<Grid gutter="md">
|
||||||
<Grid gutter="lg">
|
{kpiData.map((item) => (
|
||||||
{kpiData.map((kpi) => (
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="flex-start" mb="xs">
|
|
||||||
<Text size="sm" fw={500} c="dimmed">
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop
|
|
||||||
color: "var(--mantine-color-dimmed)", // Set color via prop
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c="dimmed" mt={2}>
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} fw={500} mb="md">
|
|
||||||
Interaksi Chatbot
|
|
||||||
</Title>
|
|
||||||
<BarChart
|
|
||||||
h={300}
|
|
||||||
data={chartData}
|
|
||||||
dataKey="day"
|
|
||||||
series={[{ name: "total", color: "blue" }]}
|
|
||||||
withLegend
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Charts and Lists Section */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
{/* Grafik Interaksi Chatbot (now Bar Chart) */}
|
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Jam Tersibuk
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<Stack gap="sm">
|
{item.title}
|
||||||
{busyHours.map((item, index) => (
|
</Text>
|
||||||
<Box key={index}>
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
<Text size="sm">{item.period}</Text>
|
{item.value}
|
||||||
<Group align="center">
|
</Text>
|
||||||
<Progress value={item.percentage} flex={1} />
|
<Group gap={4} align="flex-start">
|
||||||
<Text size="sm" fw={500}>
|
{item.trend === "positive" && (
|
||||||
{item.percentage}%
|
<TrendingUp size={14} color="#22C55E" />
|
||||||
</Text>
|
)}
|
||||||
</Group>
|
<Text
|
||||||
</Box>
|
size="xs"
|
||||||
))}
|
c={
|
||||||
</Stack>
|
item.trend === "positive"
|
||||||
|
? "green"
|
||||||
|
: dark
|
||||||
|
? "gray.4"
|
||||||
|
: "gray.5"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */}
|
{/* MAIN CHART - INTERAKSI CHATBOT */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Card
|
||||||
<Stack gap="lg">
|
p="md"
|
||||||
{/* Topik Pertanyaan Terbanyak */}
|
radius="xl"
|
||||||
<Card
|
withBorder
|
||||||
p="md"
|
bg={dark ? "#1E293B" : "white"}
|
||||||
radius="md"
|
style={{
|
||||||
withBorder
|
borderColor: dark ? "#334155" : "white",
|
||||||
bg={dark ? "#141D34" : "white"}
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
}}
|
||||||
h="100%"
|
>
|
||||||
>
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={3} fw={500} mb="md">
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Topik Pertanyaan Terbanyak
|
Interaksi Chatbot
|
||||||
</Title>
|
</Title>
|
||||||
<Stack gap="xs">
|
</Group>
|
||||||
{topTopics.map((item, index) => (
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<Group
|
<BarChart data={chartData}>
|
||||||
key={index}
|
<CartesianGrid
|
||||||
justify="space-between"
|
strokeDasharray="3 3"
|
||||||
align="center"
|
vertical={false}
|
||||||
p="xs"
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="day"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
cursor={{ fill: dark ? "#334155" : "#f3f4f6" }}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
maxBarSize={60}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* BOTTOM SECTION - 2 COLUMNS */}
|
||||||
|
<Grid gutter="lg">
|
||||||
|
{/* LEFT: TOPIK PERTANYAAN TERBANYAK */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
|
||||||
|
Topik Pertanyaan Terbanyak
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{topTopics.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
p="sm"
|
||||||
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
|
style={{
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.topic}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="darmasaba-blue"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
>
|
>
|
||||||
<Text size="sm" fw={500}>
|
{item.count}x
|
||||||
{item.topic}
|
</Badge>
|
||||||
</Text>
|
</Group>
|
||||||
<Badge variant="light" color="gray">
|
</Box>
|
||||||
{item.count}x
|
))}
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Jam Tersibuk */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Card>
|
||||||
</Grid>
|
</Grid.Col>
|
||||||
</Stack>
|
|
||||||
</Box>
|
{/* RIGHT: JAM TERSIBUK */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
|
||||||
|
Jam Tersibuk
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
{busyHours.map((item, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<Group justify="space-between" mb={5}>
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.period}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.percentage}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={item.percentage}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
color="#1E3A5F"
|
||||||
|
animated
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JennaAnalytic;
|
export default JennaAnalytic;
|
||||||
|
|||||||
@@ -125,10 +125,51 @@ const KeamananPage = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* KPI Cards */}
|
|
||||||
|
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{kpiData.map((kpi, index) => (
|
{/* Peta Keamanan CCTV */}
|
||||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((kpi, index) => (
|
||||||
|
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{kpi.subtitle}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||||
|
{kpi.value}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{kpi.title}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="light"
|
||||||
|
color={kpi.color}
|
||||||
|
size="xl"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
{kpi.icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</GridCol>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -137,119 +178,81 @@ const KeamananPage = () => {
|
|||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
||||||
<Stack gap={0}>
|
Peta Keamanan CCTV
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.subtitle}
|
|
||||||
</Text>
|
|
||||||
<Group gap="xs" align="center">
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{kpi.value}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="light"
|
|
||||||
color={kpi.color}
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
{kpi.icon}
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid gutter="md">
|
|
||||||
{/* Peta Keamanan CCTV */}
|
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Peta Keamanan CCTV
|
|
||||||
</Title>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
|
|
||||||
Titik Lokasi CCTV
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Placeholder for map */}
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
backgroundColor: dark ? "#2d3748" : "#e2e8f0",
|
|
||||||
borderRadius: "8px",
|
|
||||||
height: "400px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack align="center">
|
|
||||||
<IconMapPin
|
|
||||||
size={48}
|
|
||||||
stroke={1.5}
|
|
||||||
color={dark ? "#94a3b8" : "#64748b"}
|
|
||||||
/>
|
|
||||||
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">
|
|
||||||
Integrasi dengan Google Maps atau Mapbox akan ditampilkan di
|
|
||||||
sini
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* CCTV Locations List */}
|
|
||||||
<Stack mt="md" gap="sm">
|
|
||||||
<Title order={4} c={dark ? "dark.0" : "black"}>
|
|
||||||
Daftar CCTV
|
|
||||||
</Title>
|
</Title>
|
||||||
{cctvLocations.map((cctv, index) => (
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
|
||||||
<Card
|
Titik Lokasi CCTV
|
||||||
key={index}
|
</Text>
|
||||||
p="md"
|
|
||||||
radius="md"
|
{/* Placeholder for map */}
|
||||||
withBorder
|
<Box
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
style={{
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
backgroundColor: dark ? "#2d3748" : "#e2e8f0",
|
||||||
>
|
borderRadius: "8px",
|
||||||
<Group justify="space-between">
|
height: "400px",
|
||||||
<Stack gap={0}>
|
display: "flex",
|
||||||
<Group gap="xs">
|
alignItems: "center",
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
justifyContent: "center",
|
||||||
{cctv.id}
|
}}
|
||||||
|
>
|
||||||
|
<Stack align="center">
|
||||||
|
<IconMapPin
|
||||||
|
size={48}
|
||||||
|
stroke={1.5}
|
||||||
|
color={dark ? "#94a3b8" : "#64748b"}
|
||||||
|
/>
|
||||||
|
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">
|
||||||
|
Integrasi dengan Google Maps atau Mapbox akan ditampilkan di
|
||||||
|
sini
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* CCTV Locations List */}
|
||||||
|
<Stack mt="md" gap="sm">
|
||||||
|
<Title order={4} c={dark ? "dark.0" : "black"}>
|
||||||
|
Daftar CCTV
|
||||||
|
</Title>
|
||||||
|
{cctvLocations.map((cctv, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||||
|
{cctv.id}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
variant="dot"
|
||||||
|
color={cctv.status === "active" ? "green" : "gray"}
|
||||||
|
>
|
||||||
|
{cctv.status === "active" ? "Online" : "Offline"}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{cctv.location}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconClock size={16} stroke={1.5} />
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{cctv.lastSeen}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
|
||||||
variant="dot"
|
|
||||||
color={cctv.status === "active" ? "green" : "gray"}
|
|
||||||
>
|
|
||||||
{cctv.status === "active" ? "Online" : "Offline"}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{cctv.location}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconClock size={16} stroke={1.5} />
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{cctv.lastSeen}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Card>
|
||||||
</Card>
|
))}
|
||||||
))}
|
</Stack>
|
||||||
</Stack>
|
</Card>
|
||||||
</Card>
|
</Stack>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Daftar Laporan Keamanan */}
|
{/* Daftar Laporan Keamanan */}
|
||||||
|
|||||||
@@ -1,73 +1,70 @@
|
|||||||
import { BarChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Progress,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconCurrency,
|
CheckCircle,
|
||||||
IconTrendingDown,
|
Coins,
|
||||||
IconTrendingUp,
|
PieChart as PieChartIcon,
|
||||||
} from "@tabler/icons-react";
|
Receipt,
|
||||||
import React from "react";
|
TrendingDown,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Total APBDes",
|
title: "Total APBDes",
|
||||||
value: "Rp 5.2M",
|
value: "Rp 5.2M",
|
||||||
sub: "Tahun 2025",
|
subtitle: "Tahun 2025",
|
||||||
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
|
icon: Coins,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Realisasi",
|
title: "Realisasi",
|
||||||
value: "68%",
|
value: "68%",
|
||||||
sub: "Rp 3.5M dari 5.2M",
|
subtitle: "Rp 3.5M dari 5.2M",
|
||||||
icon: (
|
icon: CheckCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Pemasukan",
|
title: "Pemasukan",
|
||||||
value: "Rp 580jt",
|
value: "Rp 580jt",
|
||||||
sub: "Bulan ini",
|
subtitle: "Bulan ini",
|
||||||
delta: "+8%",
|
trend: "+8%",
|
||||||
deltaType: "positive",
|
icon: TrendingUp,
|
||||||
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Pengeluaran",
|
title: "Pengeluaran",
|
||||||
value: "Rp 520jt",
|
value: "Rp 520jt",
|
||||||
sub: "Bulan ini",
|
subtitle: "Bulan ini",
|
||||||
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
|
icon: TrendingDown,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Income & Expense Data
|
||||||
const incomeExpenseData = [
|
const incomeExpenseData = [
|
||||||
{ month: "Apr", income: 450, expense: 380 },
|
{ month: "Apr", income: 450, expense: 380 },
|
||||||
{ month: "Mei", income: 520, expense: 420 },
|
{ month: "Mei", income: 520, expense: 420 },
|
||||||
@@ -78,6 +75,7 @@ const incomeExpenseData = [
|
|||||||
{ month: "Okt", income: 580, expense: 520 },
|
{ month: "Okt", income: 580, expense: 520 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Sector Allocation Data
|
||||||
const allocationData = [
|
const allocationData = [
|
||||||
{ sector: "Pembangunan", amount: 1200 },
|
{ sector: "Pembangunan", amount: 1200 },
|
||||||
{ sector: "Kesehatan", amount: 800 },
|
{ sector: "Kesehatan", amount: 800 },
|
||||||
@@ -87,13 +85,7 @@ const allocationData = [
|
|||||||
{ sector: "Teknologi", amount: 300 },
|
{ sector: "Teknologi", amount: 300 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const assistanceFundData = [
|
// APBDes Report Data
|
||||||
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
|
|
||||||
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
|
|
||||||
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
|
|
||||||
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const apbdReport = {
|
const apbdReport = {
|
||||||
income: [
|
income: [
|
||||||
{ category: "Dana Desa", amount: 1800 },
|
{ category: "Dana Desa", amount: 1800 },
|
||||||
@@ -113,244 +105,410 @@ const apbdReport = {
|
|||||||
totalExpenses: 2155,
|
totalExpenses: 2155,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aid & Grants Data
|
||||||
|
const assistanceFundData = [
|
||||||
|
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
|
||||||
|
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
|
||||||
|
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
|
||||||
|
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
|
||||||
|
];
|
||||||
|
|
||||||
const KeuanganAnggaran = () => {
|
const KeuanganAnggaran = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="xl">
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
{kpiData.map((kpi) => (
|
|
||||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="flex-start" mb="xs">
|
|
||||||
<Text size="sm" fw={500} c="dimmed">
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6",
|
|
||||||
color: "var(--mantine-color-dimmed)",
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c="dimmed" mt="auto">
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Charts Section */}
|
return (
|
||||||
<Grid gutter="lg">
|
<Stack gap="lg">
|
||||||
{/* Grafik Pemasukan vs Pengeluaran */}
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((item) => (
|
||||||
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Pemasukan vs Pengeluaran
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<BarChart
|
{item.title}
|
||||||
h={300}
|
</Text>
|
||||||
data={incomeExpenseData}
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
dataKey="month"
|
{item.value}
|
||||||
series={[
|
</Text>
|
||||||
{ name: "income", color: "green", label: "Pemasukan" },
|
<Group gap={4} align="flex-start">
|
||||||
{ name: "expense", color: "red", label: "Pengeluaran" },
|
{item.trend && <TrendingUp size={14} color="#22C55E" />}
|
||||||
]}
|
<Text
|
||||||
withLegend
|
size="xs"
|
||||||
/>
|
c={item.trend ? "green" : dark ? "gray.4" : "gray.5"}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Alokasi Anggaran Per Sektor */}
|
{/* MAIN CHART SECTION */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="lg">
|
||||||
<Card
|
{/* LEFT: PEMASUKAN DAN PENGELUARAN (70%) */}
|
||||||
p="md"
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
radius="md"
|
<Card
|
||||||
withBorder
|
p="md"
|
||||||
bg={dark ? "#141D34" : "white"}
|
radius="xl"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
withBorder
|
||||||
>
|
bg={dark ? "#1E293B" : "white"}
|
||||||
<Title order={3} fw={500} mb="md">
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Pemasukan dan Pengeluaran
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={incomeExpenseData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
tickFormatter={(value) => `Rp ${value}jt`}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
formatter={(value: number | undefined) => [
|
||||||
|
`Rp ${value}jt`,
|
||||||
|
"",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="income"
|
||||||
|
stroke="#22C55E"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: "#22C55E", strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
name="Pemasukan"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="expense"
|
||||||
|
stroke="#EF4444"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: "#EF4444", strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
name="Pengeluaran"
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: ALOKASI ANGGARAN PER SEKTOR (30%) */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Alokasi Anggaran Per Sektor
|
Alokasi Anggaran Per Sektor
|
||||||
</Title>
|
</Title>
|
||||||
<BarChart
|
</Group>
|
||||||
h={300}
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
data={allocationData}
|
<BarChart data={allocationData} layout="vertical">
|
||||||
dataKey="sector"
|
<CartesianGrid
|
||||||
series={[
|
strokeDasharray="3 3"
|
||||||
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
|
horizontal={false}
|
||||||
]}
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
withLegend
|
/>
|
||||||
orientation="horizontal"
|
<XAxis
|
||||||
/>
|
type="number"
|
||||||
</Card>
|
axisLine={false}
|
||||||
</Grid.Col>
|
tickLine={false}
|
||||||
</Grid>
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
tickFormatter={(value) => `${value}`}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="sector"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 11,
|
||||||
|
}}
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
formatter={(value: number | undefined) => [
|
||||||
|
`Rp ${value}jt`,
|
||||||
|
"Jumlah",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="amount"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={30}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid gutter="lg">
|
{/* BOTTOM SECTION */}
|
||||||
{/* Dana Bantuan & Hibah */}
|
<Grid gutter="lg">
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
{/* LEFT: LAPORAN APBDES */}
|
||||||
<Card
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<Card
|
||||||
radius="md"
|
p="md"
|
||||||
withBorder
|
radius="xl"
|
||||||
bg={dark ? "#141D34" : "white"}
|
withBorder
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
>
|
style={{
|
||||||
<Title order={3} fw={500} mb="md">
|
borderColor: dark ? "#334155" : "white",
|
||||||
Dana Bantuan & Hibah
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Receipt size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Laporan APBDes
|
||||||
</Title>
|
</Title>
|
||||||
<Stack gap="sm">
|
</Group>
|
||||||
{assistanceFundData.map((fund, index) => (
|
|
||||||
<Group
|
<Grid gutter="md">
|
||||||
key={index}
|
{/* Pendapatan */}
|
||||||
justify="space-between"
|
<Grid.Col span={6}>
|
||||||
align="center"
|
<Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
|
||||||
p="sm"
|
<Title order={5} c="#22C55E" mb="sm">
|
||||||
style={{
|
Pendapatan
|
||||||
border: "1px solid var(--mantine-color-gray-3)",
|
</Title>
|
||||||
borderRadius: "var(--mantine-radius-sm)",
|
<Stack gap="xs">
|
||||||
}}
|
{apbdReport.income.map((item, index) => (
|
||||||
>
|
<Group key={index} justify="space-between">
|
||||||
|
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
|
||||||
|
{item.category}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c="#22C55E">
|
||||||
|
Rp {item.amount.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="sm"
|
||||||
|
pt="sm"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#065F46" : "#86EFAC"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c="#22C55E">
|
||||||
|
Total:
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c="#22C55E">
|
||||||
|
Rp {apbdReport.totalIncome.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* Belanja */}
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Card p="sm" radius="lg" bg={dark ? "#7F1D1D" : "#FEE2E2"}>
|
||||||
|
<Title order={5} c="#EF4444" mb="sm">
|
||||||
|
Belanja
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{apbdReport.expenses.map((item, index) => (
|
||||||
|
<Group key={index} justify="space-between">
|
||||||
|
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
|
||||||
|
{item.category}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c="#EF4444">
|
||||||
|
Rp {item.amount.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="sm"
|
||||||
|
pt="sm"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#991B1B" : "#FCA5A5"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c="#EF4444">
|
||||||
|
Total:
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c="#EF4444">
|
||||||
|
Rp {apbdReport.totalExpenses.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Saldo */}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="md"
|
||||||
|
pt="md"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
|
Saldo:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fw={700}
|
||||||
|
size="lg"
|
||||||
|
c={
|
||||||
|
apbdReport.totalIncome > apbdReport.totalExpenses
|
||||||
|
? "#22C55E"
|
||||||
|
: "#EF4444"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Rp{" "}
|
||||||
|
{(
|
||||||
|
apbdReport.totalIncome - apbdReport.totalExpenses
|
||||||
|
).toLocaleString()}
|
||||||
|
jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: DANA BANTUAN DAN HIBAH */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Coins size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Dana Bantuan dan Hibah
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{assistanceFundData.map((fund, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
p="sm"
|
||||||
|
radius="lg"
|
||||||
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
|
style={{
|
||||||
|
borderColor: "transparent",
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
{fund.source}
|
{fund.source}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
Rp {fund.amount.toLocaleString()}jt
|
Rp {fund.amount.toLocaleString()}jt
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color={fund.status === "cair" ? "green" : "yellow"}
|
color={fund.status === "cair" ? "green" : "yellow"}
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
>
|
>
|
||||||
{fund.status}
|
{fund.status === "cair" ? "Cair" : "Proses"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
</Card>
|
||||||
</Stack>
|
))}
|
||||||
</Card>
|
</Stack>
|
||||||
</Grid.Col>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
{/* Laporan APBDes */}
|
</Grid>
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
</Stack>
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} fw={500} mb="md">
|
|
||||||
Laporan APBDes
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Box mb="md">
|
|
||||||
<Title order={4} mb="sm">
|
|
||||||
Pendapatan
|
|
||||||
</Title>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{apbdReport.income.map((item, index) => (
|
|
||||||
<Group key={index} justify="space-between">
|
|
||||||
<Text size="sm">{item.category}</Text>
|
|
||||||
<Text size="sm" c="green">
|
|
||||||
Rp {item.amount.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Group justify="space-between" mt="sm">
|
|
||||||
<Text fw={700}>Total Pendapatan:</Text>
|
|
||||||
<Text fw={700} c="green">
|
|
||||||
Rp {apbdReport.totalIncome.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={4} mb="sm">
|
|
||||||
Belanja
|
|
||||||
</Title>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{apbdReport.expenses.map((item, index) => (
|
|
||||||
<Group key={index} justify="space-between">
|
|
||||||
<Text size="sm">{item.category}</Text>
|
|
||||||
<Text size="sm" c="red">
|
|
||||||
Rp {item.amount.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Group justify="space-between" mt="sm">
|
|
||||||
<Text fw={700}>Total Belanja:</Text>
|
|
||||||
<Text fw={700} c="red">
|
|
||||||
Rp {apbdReport.totalExpenses.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
mt="md"
|
|
||||||
pt="md"
|
|
||||||
style={{ borderTop: "1px solid var(--mantine-color-gray-3)" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={700}>Saldo:</Text>
|
|
||||||
<Text
|
|
||||||
fw={700}
|
|
||||||
c={
|
|
||||||
apbdReport.totalIncome > apbdReport.totalExpenses
|
|
||||||
? "green"
|
|
||||||
: "red"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Rp{" "}
|
|
||||||
{(
|
|
||||||
apbdReport.totalIncome - apbdReport.totalExpenses
|
|
||||||
).toLocaleString()}
|
|
||||||
jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,537 +1,97 @@
|
|||||||
import {
|
import { Grid, Stack } from "@mantine/core";
|
||||||
ActionIcon,
|
import { ActivityCard } from "./kinerja-divisi/activity-card";
|
||||||
Box,
|
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||||
Card,
|
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
||||||
Divider,
|
import { DivisionList } from "./kinerja-divisi/division-list";
|
||||||
Grid,
|
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
||||||
GridCol,
|
import { EventCard } from "./kinerja-divisi/event-card";
|
||||||
Group,
|
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
||||||
List,
|
|
||||||
Badge as MantineBadge,
|
// Data for program kegiatan (Section 1)
|
||||||
Progress as MantineProgress,
|
const programKegiatanData = [
|
||||||
Skeleton,
|
{
|
||||||
Stack,
|
title: "Rakor 2025",
|
||||||
Text,
|
date: "3 Juli 2025",
|
||||||
ThemeIcon,
|
progress: 90,
|
||||||
Title,
|
status: "Selesai" as const,
|
||||||
useMantineColorScheme,
|
},
|
||||||
} from "@mantine/core";
|
{
|
||||||
import {
|
title: "Pemutakhiran Indeks Desa",
|
||||||
Bar,
|
date: "3 Juli 2025",
|
||||||
BarChart,
|
progress: 85,
|
||||||
CartesianGrid,
|
status: "Selesai" as const,
|
||||||
Cell,
|
},
|
||||||
Pie,
|
{
|
||||||
PieChart,
|
title: "Mengurus Akta Cerai Warga",
|
||||||
ResponsiveContainer,
|
date: "3 Juli 2025",
|
||||||
Tooltip,
|
progress: 80,
|
||||||
XAxis,
|
status: "Selesai" as const,
|
||||||
YAxis,
|
},
|
||||||
} from "recharts";
|
{
|
||||||
import { Button } from "@/components/ui/button";
|
title: "Pasek 7 Desa Adat",
|
||||||
|
date: "3 Juli 2025",
|
||||||
|
progress: 92,
|
||||||
|
status: "Selesai" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Data for arsip digital (Section 5)
|
||||||
|
const archiveData = [
|
||||||
|
{ name: "Surat Keputusan" },
|
||||||
|
{ name: "Dokumentasi" },
|
||||||
|
{ name: "Laporan Keuangan" },
|
||||||
|
{ name: "Notulensi Rapat" },
|
||||||
|
];
|
||||||
|
|
||||||
const KinerjaDivisi = () => {
|
const KinerjaDivisi = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const dark = colorScheme === "dark";
|
|
||||||
|
|
||||||
// Data for division progress chart
|
|
||||||
const divisionProgressData = [
|
|
||||||
{ name: "Sekretariat", selesai: 12, berjalan: 5, tertunda: 2 },
|
|
||||||
{ name: "Keuangan", selesai: 8, berjalan: 7, tertunda: 1 },
|
|
||||||
{ name: "Sosial", selesai: 10, berjalan: 3, tertunda: 4 },
|
|
||||||
{ name: "Humas", selesai: 6, berjalan: 9, tertunda: 3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Division task summaries
|
|
||||||
const divisionTasks = [
|
|
||||||
{
|
|
||||||
name: "Sekretariat",
|
|
||||||
tasks: [
|
|
||||||
{ title: "Laporan Bulanan", status: "selesai" },
|
|
||||||
{ title: "Arsip Dokumen", status: "berjalan" },
|
|
||||||
{ title: "Undangan Rapat", status: "tertunda" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Keuangan",
|
|
||||||
tasks: [
|
|
||||||
{ title: "Laporan APBDes", status: "selesai" },
|
|
||||||
{ title: "Verifikasi Dana", status: "tertunda" },
|
|
||||||
{ title: "Pengeluaran Harian", status: "berjalan" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sosial",
|
|
||||||
tasks: [
|
|
||||||
{ title: "Program Bantuan", status: "selesai" },
|
|
||||||
{ title: "Kegiatan Posyandu", status: "berjalan" },
|
|
||||||
{ title: "Monitoring Stunting", status: "tertunda" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Humas",
|
|
||||||
tasks: [
|
|
||||||
{ title: "Publikasi Kegiatan", status: "selesai" },
|
|
||||||
{ title: "Koordinasi Media", status: "berjalan" },
|
|
||||||
{ title: "Laporan Kegiatan", status: "tertunda" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Archive items
|
|
||||||
const archiveItems = [
|
|
||||||
{ name: "Surat Keputusan", count: 12 },
|
|
||||||
{ name: "Laporan Keuangan", count: 8 },
|
|
||||||
{ name: "Dokumentasi", count: 24 },
|
|
||||||
{ name: "Notulensi Rapat", count: 15 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Activity progress
|
|
||||||
const activityProgress = [
|
|
||||||
{
|
|
||||||
name: "Pembangunan Jalan",
|
|
||||||
progress: 75,
|
|
||||||
date: "15 Feb 2026",
|
|
||||||
status: "berjalan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Posyandu Bulanan",
|
|
||||||
progress: 100,
|
|
||||||
date: "10 Feb 2026",
|
|
||||||
status: "selesai",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vaksinasi Massal",
|
|
||||||
progress: 45,
|
|
||||||
date: "20 Feb 2026",
|
|
||||||
status: "berjalan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Festival Budaya",
|
|
||||||
progress: 20,
|
|
||||||
date: "5 Mar 2026",
|
|
||||||
status: "berjalan",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Document statistics
|
|
||||||
const documentStats = [
|
|
||||||
{ name: "Gambar", value: 42 },
|
|
||||||
{ name: "Dokumen", value: 87 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Activity progress statistics
|
|
||||||
const activityProgressStats = [
|
|
||||||
{ name: "Selesai", value: 12, fill: "#10B981" },
|
|
||||||
{ name: "Dikerjakan", value: 8, fill: "#F59E0B" },
|
|
||||||
{ name: "Segera Dikerjakan", value: 5, fill: "#EF4444" },
|
|
||||||
{ name: "Dibatalkan", value: 2, fill: "#6B7280" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COLORS = ["#10B981", "#F59E0B", "#EF4444", "#6B7280"];
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
|
||||||
selesai: "green",
|
|
||||||
berjalan: "blue",
|
|
||||||
tertunda: "red",
|
|
||||||
proses: "yellow",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Discussion data
|
|
||||||
const discussions = [
|
|
||||||
{
|
|
||||||
title: "Pembahasan APBDes 2026",
|
|
||||||
sender: "Kepala Desa",
|
|
||||||
timestamp: "2 jam yang lalu",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Kegiatan Posyandu",
|
|
||||||
sender: "Divisi Sosial",
|
|
||||||
timestamp: "5 jam yang lalu",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Festival Budaya",
|
|
||||||
sender: "Divisi Humas",
|
|
||||||
timestamp: "1 hari yang lalu",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Today's agenda
|
|
||||||
const todayAgenda = [
|
|
||||||
{ time: "09:00", event: "Rapat Evaluasi Bulanan" },
|
|
||||||
{ time: "14:00", event: "Koordinasi Program Bantuan" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Grafik Progres Tugas per Divisi */}
|
{/* SECTION 1 — PROGRAM KEGIATAN */}
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Grafik Progres Tugas per Divisi
|
|
||||||
</Title>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<BarChart data={divisionProgressData}>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
vertical={false}
|
|
||||||
stroke={dark ? "#141D34" : "white"}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{
|
|
||||||
fill: dark
|
|
||||||
? "var(--mantine-color-text)"
|
|
||||||
: "var(--mantine-color-text)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{
|
|
||||||
fill: dark
|
|
||||||
? "var(--mantine-color-text)"
|
|
||||||
: "var(--mantine-color-text)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={
|
|
||||||
dark
|
|
||||||
? {
|
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="selesai"
|
|
||||||
stackId="a"
|
|
||||||
fill="#10B981"
|
|
||||||
name="Selesai"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="berjalan"
|
|
||||||
stackId="a"
|
|
||||||
fill="#3B82F6"
|
|
||||||
name="Berjalan"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="tertunda"
|
|
||||||
stackId="a"
|
|
||||||
fill="#EF4444"
|
|
||||||
name="Tertunda"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Ringkasan Tugas per Divisi */}
|
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{divisionTasks.map((division, index) => (
|
{programKegiatanData.map((kegiatan, index) => (
|
||||||
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
<Grid.Col key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
||||||
<Card
|
<ActivityCard
|
||||||
p="md"
|
title={kegiatan.title}
|
||||||
radius="md"
|
date={kegiatan.date}
|
||||||
withBorder
|
progress={kegiatan.progress}
|
||||||
bg={dark ? "#141D34" : "white"}
|
status={kegiatan.status}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
/>
|
||||||
h="100%"
|
</Grid.Col>
|
||||||
>
|
|
||||||
<Title order={4} mb="sm" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
{division.name}
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{division.tasks.map((task, taskIndex) => (
|
|
||||||
<Box key={taskIndex}>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
{task.title}
|
|
||||||
</Text>
|
|
||||||
<MantineBadge
|
|
||||||
color={STATUS_COLORS[task.status] || "gray"}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
{task.status}
|
|
||||||
</MantineBadge>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Arsip Digital Perangkat Desa */}
|
{/* SECTION 2 — GRID DASHBOARD (3 Columns) */}
|
||||||
<Card
|
<Grid gutter="lg">
|
||||||
p="md"
|
{/* Left Column - Division List */}
|
||||||
radius="md"
|
<Grid.Col span={{ base: 12, lg: 3 }}>
|
||||||
withBorder
|
<DivisionList />
|
||||||
bg={dark ? "#141D34" : "white"}
|
</Grid.Col>
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Arsip Digital Perangkat Desa
|
|
||||||
</Title>
|
|
||||||
<Grid gutter="md">
|
|
||||||
{archiveItems.map((item, index) => (
|
|
||||||
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
<Text c={dark ? "white" : "darmasaba-navy"} fw={700}>
|
|
||||||
{item.count}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Kartu Progres Kegiatan */}
|
{/* Middle Column - Document Chart */}
|
||||||
<Card
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||||
p="md"
|
<DocumentChart />
|
||||||
radius="md"
|
</Grid.Col>
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Progres Kegiatan / Program
|
|
||||||
</Title>
|
|
||||||
<Stack gap="md">
|
|
||||||
{activityProgress.map((activity, index) => (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" mb="sm">
|
|
||||||
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
|
|
||||||
{activity.name}
|
|
||||||
</Text>
|
|
||||||
<MantineBadge
|
|
||||||
color={STATUS_COLORS[activity.status] || "gray"}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
{activity.status}
|
|
||||||
</MantineBadge>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<MantineProgress
|
|
||||||
value={activity.progress}
|
|
||||||
size="sm"
|
|
||||||
radius="xl"
|
|
||||||
color={activity.progress === 100 ? "green" : "blue"}
|
|
||||||
w="calc(100% - 80px)"
|
|
||||||
/>
|
|
||||||
<Text size="sm" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
{activity.progress}%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" mt="sm">
|
|
||||||
{activity.date}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Statistik Dokumen & Progres Kegiatan */}
|
{/* Right Column - Progress Chart */}
|
||||||
<Grid gutter="md">
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
<ProgressChart />
|
||||||
<Card
|
</Grid.Col>
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Jumlah Dokumen
|
|
||||||
</Title>
|
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
|
||||||
<BarChart data={documentStats}>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
vertical={false}
|
|
||||||
stroke={dark ? "#141D34" : "white"}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{
|
|
||||||
fill: dark
|
|
||||||
? "var(--mantine-color-text)"
|
|
||||||
: "var(--mantine-color-text)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{
|
|
||||||
fill: dark
|
|
||||||
? "var(--mantine-color-text)"
|
|
||||||
: "var(--mantine-color-text)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={
|
|
||||||
dark
|
|
||||||
? {
|
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="value"
|
|
||||||
fill={
|
|
||||||
dark
|
|
||||||
? "var(--mantine-color-blue-6)"
|
|
||||||
: "var(--mantine-color-blue-filled)"
|
|
||||||
}
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
|
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Progres Kegiatan
|
|
||||||
</Title>
|
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
|
||||||
<PieChart
|
|
||||||
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
|
|
||||||
>
|
|
||||||
<Pie
|
|
||||||
data={activityProgressStats}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine
|
|
||||||
outerRadius={65}
|
|
||||||
dataKey="value"
|
|
||||||
label={({ name, percent }) =>
|
|
||||||
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={
|
|
||||||
dark
|
|
||||||
? {
|
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Diskusi Internal */}
|
{/* SECTION 3 — DISCUSSION PANEL */}
|
||||||
<Card
|
<DiscussionPanel />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
Diskusi Internal
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{discussions.map((discussion, index) => (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
|
|
||||||
{discussion.title}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{discussion.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{discussion.sender}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Agenda / Acara Hari Ini */}
|
{/* SECTION 4 — ACARA HARI INI */}
|
||||||
<Card
|
<EventCard />
|
||||||
p="md"
|
|
||||||
radius="md"
|
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
|
||||||
withBorder
|
<Grid gutter="md">
|
||||||
bg={dark ? "#141D34" : "white"}
|
{archiveData.map((item, index) => (
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
<Grid.Col key={index} span={{ base: 12, md: 6 }}>
|
||||||
>
|
<ArchiveCard item={item} />
|
||||||
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
|
</Grid.Col>
|
||||||
Agenda / Acara Hari Ini
|
))}
|
||||||
</Title>
|
</Grid>
|
||||||
{todayAgenda.length > 0 ? (
|
|
||||||
<Stack gap="sm">
|
|
||||||
{todayAgenda.map((agenda, index) => (
|
|
||||||
<Group key={index} align="flex-start">
|
|
||||||
<Box w={60}>
|
|
||||||
<Text c="dimmed">{agenda.time}</Text>
|
|
||||||
</Box>
|
|
||||||
<Divider orientation="vertical" mx="sm" />
|
|
||||||
<Text c={dark ? "white" : "darmasaba-navy"}>
|
|
||||||
{agenda.event}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Text c="dimmed" ta="center" py="md">
|
|
||||||
Tidak ada acara hari ini
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
89
src/components/kinerja-divisi/activity-card.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Box, Card, Group, Progress, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
interface ActivityCardProps {
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
progress: number;
|
||||||
|
status: "Selesai" | "Berjalan" | "Tertunda";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivityCard({
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
}: ActivityCardProps) {
|
||||||
|
const getStatusColor = () => {
|
||||||
|
switch (status) {
|
||||||
|
case "Selesai":
|
||||||
|
return "#22C55E";
|
||||||
|
case "Berjalan":
|
||||||
|
return "#3B82F6";
|
||||||
|
case "Tertunda":
|
||||||
|
return "#EF4444";
|
||||||
|
default:
|
||||||
|
return "#9CA3AF";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="xl"
|
||||||
|
p={0}
|
||||||
|
withBorder={false}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#F3F4F6",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 🔵 HEADER */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#1E3A5F",
|
||||||
|
padding: "16px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text c="white" fw={700} size="md">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* CONTENT */}
|
||||||
|
<Box p="md">
|
||||||
|
{/* PROGRESS */}
|
||||||
|
<Progress
|
||||||
|
value={progress}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
color="orange"
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<Group justify="space-between" mt="md">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{date}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: getStatusColor(),
|
||||||
|
color: "white",
|
||||||
|
padding: "4px 12px",
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/kinerja-divisi/archive-card.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Card, Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { FileText } from "lucide-react";
|
||||||
|
|
||||||
|
interface ArchiveItem {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArchiveCardProps {
|
||||||
|
item: ArchiveItem;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "transform 0.2s, box-shadow 0.2s",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Group gap="md">
|
||||||
|
<FileText size={32} color={dark ? "#60A5FA" : "#3B82F6"} />
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/components/kinerja-divisi/discussion-panel.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { MessageCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface DiscussionItem {
|
||||||
|
message: string;
|
||||||
|
sender: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discussions: DiscussionItem[] = [
|
||||||
|
{
|
||||||
|
message: "Kepada Pelayanan, mohon di cek...",
|
||||||
|
sender: "I.B Surya Prabhawa Manu",
|
||||||
|
date: "12 Apr 2025",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Kepada staf perencanaan @suar...",
|
||||||
|
sender: "Ni Nyoman Yuliani",
|
||||||
|
date: "14 Jun 2025",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ijin atau mohon kepada KBD sar...",
|
||||||
|
sender: "Ni Wayan Martini",
|
||||||
|
date: "12 Apr 2025",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DiscussionPanel() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
Diskusi
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{discussions.map((discussion, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
p="sm"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "#F1F5F9",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
c={dark ? "white" : "#1E3A5F"}
|
||||||
|
fw={500}
|
||||||
|
mb="xs"
|
||||||
|
lineClamp={2}
|
||||||
|
>
|
||||||
|
{discussion.message}
|
||||||
|
</Text>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{discussion.sender}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{discussion.date}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/kinerja-divisi/division-list.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
|
interface DivisionItem {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const divisionData: DivisionItem[] = [
|
||||||
|
{ name: "Kesejahteraan", count: 37 },
|
||||||
|
{ name: "Pemerintahan", count: 26 },
|
||||||
|
{ name: "Keuangan", count: 17 },
|
||||||
|
{ name: "Sekretaris Desa", count: 15 },
|
||||||
|
{ name: "Tata Usaha TK", count: 14 },
|
||||||
|
{ name: "Perangkat Kewilayahan", count: 12 },
|
||||||
|
{ name: "Pelayanan", count: 10 },
|
||||||
|
{ name: "Perencanaan", count: 9 },
|
||||||
|
{ name: "Tata Usaha & Umum", count: 7 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DivisionList() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||||
|
Divisi Teraktif
|
||||||
|
</Text>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{divisionData.map((division, index) => (
|
||||||
|
<Group
|
||||||
|
key={index}
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
style={{
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: dark ? "#334155" : "#F1F5F9",
|
||||||
|
transition: "background-color 0.2s",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
{division.name}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
{division.count}
|
||||||
|
</Text>
|
||||||
|
<ChevronRight size={16} color={dark ? "#94A3B8" : "#64748B"} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/components/kinerja-divisi/document-chart.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Card, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
const documentData = [
|
||||||
|
{ name: "Gambar", jumlah: 300, color: "#FACC15" },
|
||||||
|
{ name: "Dokumen", jumlah: 310, color: "#22C55E" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DocumentChart() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||||
|
Jumlah Dokumen
|
||||||
|
</Text>
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<BarChart data={documentData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
|
||||||
|
{documentData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/kinerja-divisi/event-card.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
|
interface AgendaItem {
|
||||||
|
time: string;
|
||||||
|
event: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventCardProps {
|
||||||
|
agendas?: AgendaItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventCard({ agendas = [] }: EventCardProps) {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
Acara Hari Ini
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{agendas.length > 0 ? (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{agendas.map((agenda, index) => (
|
||||||
|
<Group key={index} align="flex-start" gap="md">
|
||||||
|
<Box w={60}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
{agenda.time}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
|
||||||
|
{agenda.event}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" ta="center" py="md">
|
||||||
|
Tidak ada acara hari ini
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/kinerja-divisi/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { ActivityCard } from "./activity-card";
|
||||||
|
export { ArchiveCard } from "./archive-card";
|
||||||
|
export { DiscussionPanel } from "./discussion-panel";
|
||||||
|
export { DivisionList } from "./division-list";
|
||||||
|
export { DocumentChart } from "./document-chart";
|
||||||
|
export { EventCard } from "./event-card";
|
||||||
|
export { ProgressChart } from "./progress-chart";
|
||||||
84
src/components/kinerja-divisi/progress-chart.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||||
|
|
||||||
|
const progressData = [
|
||||||
|
{ name: "Selesai", value: 83.33, color: "#22C55E" },
|
||||||
|
{ name: "Dikerjakan", value: 16.67, color: "#F59E0B" },
|
||||||
|
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
|
||||||
|
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProgressChart() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: dark
|
||||||
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||||
|
Progres Kegiatan
|
||||||
|
</Text>
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={progressData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{progressData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Stack gap="xs" mt="md">
|
||||||
|
{progressData.map((item, index) => (
|
||||||
|
<Group key={index} justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Box
|
||||||
|
w={12}
|
||||||
|
h={12}
|
||||||
|
style={{ backgroundColor: item.color, borderRadius: 2 }}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.value.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Collapse,
|
Collapse,
|
||||||
Group,
|
Group,
|
||||||
|
Image,
|
||||||
Input,
|
Input,
|
||||||
NavLink as MantineNavLink,
|
NavLink as MantineNavLink,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -27,35 +28,29 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
|
|
||||||
// State for settings submenu collapse
|
// State for settings submenu collapse
|
||||||
const [settingsOpen, setSettingsOpen] = useState(
|
const [settingsOpen, setSettingsOpen] = useState(
|
||||||
location.pathname.startsWith("/dashboard/pengaturan"),
|
location.pathname.startsWith("/pengaturan"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Define menu items with their paths
|
// Define menu items with their paths
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ name: "Beranda", path: "/dashboard" },
|
{ name: "Beranda", path: "/" },
|
||||||
{ name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
|
{ name: "Kinerja Divisi", path: "/kinerja-divisi" },
|
||||||
{
|
{ name: "Pengaduan & Layanan Publik", path: "/pengaduan-layanan-publik" },
|
||||||
name: "Pengaduan & Layanan Publik",
|
{ name: "Jenna Analytic", path: "/jenna-analytic" },
|
||||||
path: "/dashboard/pengaduan-layanan-publik",
|
{ name: "Demografi & Kependudukan", path: "/demografi-pekerjaan" },
|
||||||
},
|
{ name: "Keuangan & Anggaran", path: "/keuangan-anggaran" },
|
||||||
{ name: "Jenna Analytic", path: "/dashboard/jenna-analytic" },
|
{ name: "Bumdes & UMKM Desa", path: "/bumdes" },
|
||||||
{
|
{ name: "Sosial", path: "/sosial" },
|
||||||
name: "Demografi & Kependudukan",
|
{ name: "Keamanan", path: "/keamanan" },
|
||||||
path: "/dashboard/demografi-pekerjaan",
|
{ name: "Bantuan", path: "/bantuan" },
|
||||||
},
|
|
||||||
{ name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" },
|
|
||||||
{ name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
|
|
||||||
{ name: "Sosial", path: "/dashboard/sosial" },
|
|
||||||
{ name: "Keamanan", path: "/dashboard/keamanan" },
|
|
||||||
{ name: "Bantuan", path: "/dashboard/bantuan" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Settings submenu items
|
// Settings submenu items
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
{ name: "Umum", path: "/dashboard/pengaturan/umum" },
|
{ name: "Umum", path: "/pengaturan/umum" },
|
||||||
{ name: "Notifikasi", path: "/dashboard/pengaturan/notifikasi" },
|
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
|
||||||
{ name: "Keamanan", path: "/dashboard/pengaturan/keamanan" },
|
{ name: "Keamanan", path: "/pengaturan/keamanan" },
|
||||||
{ name: "Akses & Tim", path: "/dashboard/pengaturan/akses-dan-tim" },
|
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if any settings submenu is active
|
// Check if any settings submenu is active
|
||||||
@@ -66,30 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<Box className={className}>
|
<Box className={className}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Box
|
<Image src={dark ? "/white.png" : "/light-mode.png"} alt="Logo" />
|
||||||
p="md"
|
|
||||||
style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
|
|
||||||
>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Badge
|
|
||||||
color="dark"
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
py="xs"
|
|
||||||
px="md"
|
|
||||||
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
|
|
||||||
>
|
|
||||||
DESA
|
|
||||||
</Badge>
|
|
||||||
<Badge color="green" variant="filled" size="md" radius="md">
|
|
||||||
+
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
<Text size="xs" c="dimmed" mt="xs">
|
|
||||||
Digitalisasi Desa Transparansi Kerja
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
|
|||||||
@@ -1,463 +1,45 @@
|
|||||||
import {
|
import { Grid, GridCol, Stack } from "@mantine/core";
|
||||||
Badge,
|
import { SummaryCards } from "./sosial/summary-cards";
|
||||||
Card,
|
import { HealthStats } from "./sosial/health-stats";
|
||||||
Grid,
|
import { PosyanduSchedule } from "./sosial/posyandu-schedule";
|
||||||
GridCol,
|
import { Pendidikan } from "./sosial/pendidikan";
|
||||||
Group,
|
import { Beasiswa } from "./sosial/beasiswa";
|
||||||
List,
|
import { EventCalendar } from "./sosial/event-calendar";
|
||||||
Progress,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconAward,
|
|
||||||
IconBabyCarriage,
|
|
||||||
IconBook,
|
|
||||||
IconCalendarEvent,
|
|
||||||
IconHeartbeat,
|
|
||||||
IconMedicalCross,
|
|
||||||
IconSchool,
|
|
||||||
IconStethoscope,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const SosialPage = () => {
|
const SosialPage = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const dark = colorScheme === "dark";
|
|
||||||
|
|
||||||
// Sample data for health statistics
|
|
||||||
const healthStats = {
|
|
||||||
ibuHamil: 87,
|
|
||||||
balita: 342,
|
|
||||||
alertStunting: 12,
|
|
||||||
posyanduAktif: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sample data for health progress
|
|
||||||
const healthProgress = [
|
|
||||||
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
|
|
||||||
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
|
|
||||||
{ label: "Gizi Baik", value: 86, color: "teal" },
|
|
||||||
{ label: "Target Stunting", value: 14, color: "red" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for posyandu schedule
|
|
||||||
const posyanduSchedule = [
|
|
||||||
{
|
|
||||||
nama: "Posyandu Mawar",
|
|
||||||
tanggal: "Senin, 15 Feb 2026",
|
|
||||||
jam: "08:00 - 11:00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nama: "Posyandu Melati",
|
|
||||||
tanggal: "Selasa, 16 Feb 2026",
|
|
||||||
jam: "08:00 - 11:00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nama: "Posyandu Dahlia",
|
|
||||||
tanggal: "Rabu, 17 Feb 2026",
|
|
||||||
jam: "08:00 - 11:00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nama: "Posyandu Anggrek",
|
|
||||||
tanggal: "Kamis, 18 Feb 2026",
|
|
||||||
jam: "08:00 - 11:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for education stats
|
|
||||||
const educationStats = {
|
|
||||||
siswa: {
|
|
||||||
tk: 125,
|
|
||||||
sd: 480,
|
|
||||||
smp: 210,
|
|
||||||
sma: 150,
|
|
||||||
},
|
|
||||||
sekolah: {
|
|
||||||
jumlah: 8,
|
|
||||||
guru: 42,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sample data for scholarships
|
|
||||||
const scholarshipData = {
|
|
||||||
penerima: 45,
|
|
||||||
dana: "Rp 1.200.000.000",
|
|
||||||
tahunAjaran: "2025/2026",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sample data for cultural events
|
|
||||||
const culturalEvents = [
|
|
||||||
{
|
|
||||||
nama: "Hari Kesaktian Pancasila",
|
|
||||||
tanggal: "1 Oktober 2025",
|
|
||||||
lokasi: "Balai Desa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nama: "Festival Budaya Desa",
|
|
||||||
tanggal: "20 Mei 2026",
|
|
||||||
lokasi: "Lapangan Desa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nama: "Perayaan HUT Desa",
|
|
||||||
tanggal: "17 Agustus 2026",
|
|
||||||
lokasi: "Balai Desa",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Health Statistics Cards */}
|
{/* Top Summary Cards - 4 Grid */}
|
||||||
|
<SummaryCards />
|
||||||
|
|
||||||
|
{/* Second Row - 2 Column Grid */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
{/* Left - Statistik Kesehatan */}
|
||||||
<Card
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<HealthStats />
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Ibu Hamil Aktif
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{healthStats.ibuHamil}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="light"
|
|
||||||
color="darmasaba-blue"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconHeartbeat size={24} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
{/* Right - Jadwal Posyandu */}
|
||||||
<Card
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<PosyanduSchedule />
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Balita Terdaftar
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{healthStats.balita}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="light"
|
|
||||||
color="darmasaba-success"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconBabyCarriage size={24} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
|
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Alert Stunting
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c="red">
|
|
||||||
{healthStats.alertStunting}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon variant="light" color="red" size="xl" radius="xl">
|
|
||||||
<IconStethoscope size={24} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
|
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Posyandu Aktif
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{healthStats.posyanduAktif}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="light"
|
|
||||||
color="darmasaba-warning"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconMedicalCross size={24} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Health Progress Bars */}
|
{/* Third Row - 2 Column Grid */}
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Statistik Kesehatan
|
|
||||||
</Title>
|
|
||||||
<Stack gap="md">
|
|
||||||
{healthProgress.map((item, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Group justify="space-between" mb={5}>
|
|
||||||
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.value}%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Progress
|
|
||||||
value={item.value}
|
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
color={item.color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{/* Jadwal Posyandu */}
|
{/* Left - Pendidikan */}
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
<Card
|
<Pendidikan />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Jadwal Posyandu
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{posyanduSchedule.map((item, index) => (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.nama}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.tanggal}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Badge variant="light" color="darmasaba-blue">
|
|
||||||
{item.jam}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Pendidikan */}
|
{/* Right - Beasiswa Desa */}
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
<Card
|
<Beasiswa />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Pendidikan
|
|
||||||
</Title>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
TK / PAUD
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.siswa.tk}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
SD
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.siswa.sd}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
SMP
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.siswa.smp}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
SMA
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.siswa.sma}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
p="md"
|
|
||||||
mt="md"
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
Jumlah Lembaga Pendidikan
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.sekolah.jumlah}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between" mt="sm">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
Jumlah Tenaga Pengajar
|
|
||||||
</Text>
|
|
||||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{educationStats.sekolah.guru}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid gutter="md">
|
{/* Bottom Section - Event Budaya */}
|
||||||
{/* Beasiswa Desa */}
|
<EventCalendar />
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Beasiswa Desa
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
Penerima: {scholarshipData.penerima}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<ThemeIcon
|
|
||||||
variant="light"
|
|
||||||
color="darmasaba-success"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconAward size={24} />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Group>
|
|
||||||
<Text mt="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Dana Tersalurkan:{" "}
|
|
||||||
<Text span fw={700}>
|
|
||||||
{scholarshipData.dana}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Text mt="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Tahun Ajaran: {scholarshipData.tahunAjaran}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
|
|
||||||
{/* Kalender Event Budaya */}
|
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Kalender Event Budaya
|
|
||||||
</Title>
|
|
||||||
<List spacing="sm">
|
|
||||||
{culturalEvents.map((event, index) => (
|
|
||||||
<List.Item
|
|
||||||
key={index}
|
|
||||||
icon={
|
|
||||||
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
|
|
||||||
<IconCalendarEvent size={12} />
|
|
||||||
</ThemeIcon>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{event.nama}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{event.lokasi}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{event.tanggal}
|
|
||||||
</Text>
|
|
||||||
</List.Item>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
69
src/components/sosial/beasiswa.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Card, Group, Stack, Text, ThemeIcon, Title } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { IconAward } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface ScholarshipData {
|
||||||
|
penerima: number;
|
||||||
|
dana: string;
|
||||||
|
tahunAjaran: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeasiswaProps {
|
||||||
|
data?: ScholarshipData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Beasiswa = ({ data }: BeasiswaProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: ScholarshipData = {
|
||||||
|
penerima: 45,
|
||||||
|
dana: "Rp 1.200.000.000",
|
||||||
|
tahunAjaran: "2025/2026",
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
h={"100%"}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
Beasiswa Desa
|
||||||
|
</Text>
|
||||||
|
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Penerima: {displayData.penerima}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="light"
|
||||||
|
color="darmasaba-success"
|
||||||
|
size="xl"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<IconAward size={24} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="xs" mt="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text c={dark ? "dark.3" : "dimmed"}>Dana Tersalurkan:</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.dana}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text c={dark ? "dark.3" : "dimmed"}>Tahun Ajaran:</Text>
|
||||||
|
<Text c={dark ? "dark.0" : "#1e3a5f"}>{displayData.tahunAjaran}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
src/components/sosial/event-calendar.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Card, Group, Stack, Text, Title, ThemeIcon } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface EventItem {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
tanggal: string;
|
||||||
|
lokasi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventCalendarProps {
|
||||||
|
data?: EventItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventCalendar = ({ data }: EventCalendarProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: EventItem[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
nama: "Hari Kesaktian Pancasila",
|
||||||
|
tanggal: "1 Oktober 2025",
|
||||||
|
lokasi: "Balai Desa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
nama: "Festival Budaya Desa",
|
||||||
|
tanggal: "20 Mei 2026",
|
||||||
|
lokasi: "Lapangan Desa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
nama: "Perayaan HUT Desa",
|
||||||
|
tanggal: "17 Agustus 2026",
|
||||||
|
lokasi: "Balai Desa",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Kalender Event Budaya
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayData.map((event) => (
|
||||||
|
<Card
|
||||||
|
key={event.id}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Group gap="sm" align="center">
|
||||||
|
<ThemeIcon
|
||||||
|
color="darmasaba-blue"
|
||||||
|
size="md"
|
||||||
|
radius="xl"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
<IconCalendarEvent size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{event.nama}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{event.lokasi}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group pl={36}>
|
||||||
|
<Text size="sm" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{event.tanggal}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
66
src/components/sosial/health-stats.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Card, Group, Progress, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
|
||||||
|
interface HealthProgressItem {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HealthStatsProps {
|
||||||
|
data?: HealthProgressItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthStats = ({ data }: HealthStatsProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: HealthProgressItem[] = [
|
||||||
|
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
|
||||||
|
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
|
||||||
|
{ label: "Gizi Baik", value: 86, color: "teal" },
|
||||||
|
{ label: "Target Stunting", value: 14, color: "red" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
h={'100%'}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Statistik Kesehatan
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
{displayData.map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<Group justify="space-between" mb={5}>
|
||||||
|
<Text size="sm" fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
c={item.color === "red" ? "red" : dark ? "dark.0" : "#1e3a5f"}
|
||||||
|
>
|
||||||
|
{item.value}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={item.value}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
color={item.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
114
src/components/sosial/pendidikan.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
|
||||||
|
interface EducationData {
|
||||||
|
siswa: {
|
||||||
|
tk: number;
|
||||||
|
sd: number;
|
||||||
|
smp: number;
|
||||||
|
sma: number;
|
||||||
|
};
|
||||||
|
sekolah: {
|
||||||
|
jumlah: number;
|
||||||
|
guru: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendidikanProps {
|
||||||
|
data?: EducationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pendidikan = ({ data }: PendidikanProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: EducationData = {
|
||||||
|
siswa: {
|
||||||
|
tk: 125,
|
||||||
|
sd: 480,
|
||||||
|
smp: 210,
|
||||||
|
sma: 150,
|
||||||
|
},
|
||||||
|
sekolah: {
|
||||||
|
jumlah: 8,
|
||||||
|
guru: 42,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Pendidikan
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
TK / PAUD
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.tk}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SD
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.sd}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SMP
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.smp}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SMA
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.sma}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
mt="md"
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jumlah Lembaga Pendidikan
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.sekolah.jumlah}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between" mt="sm">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jumlah Tenaga Pengajar
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.sekolah.guru}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
89
src/components/sosial/posyandu-schedule.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Badge, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
|
||||||
|
interface PosyanduItem {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
tanggal: string;
|
||||||
|
jam: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PosyanduScheduleProps {
|
||||||
|
data?: PosyanduItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PosyanduSchedule = ({ data }: PosyanduScheduleProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: PosyanduItem[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
nama: "Posyandu Mawar",
|
||||||
|
tanggal: "Senin, 15 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
nama: "Posyandu Melati",
|
||||||
|
tanggal: "Selasa, 16 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
nama: "Posyandu Dahlia",
|
||||||
|
tanggal: "Rabu, 17 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
nama: "Posyandu Anggrek",
|
||||||
|
tanggal: "Kamis, 18 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jadwal Posyandu
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayData.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{item.nama}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{item.tanggal}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Badge variant="light" color="darmasaba-blue" size="md">
|
||||||
|
{item.jam}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
132
src/components/sosial/summary-cards.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Card, Grid, GridCol, Group, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconBabyCarriage,
|
||||||
|
IconHeartbeat,
|
||||||
|
IconMedicalCross,
|
||||||
|
IconStethoscope,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface SummaryCardProps {
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SummaryCard = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
highlight = false,
|
||||||
|
backgroundColor,
|
||||||
|
}: SummaryCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="xl"
|
||||||
|
fw={700}
|
||||||
|
c={highlight ? "red" : dark ? "dark.0" : "#1e3a5f"}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon bg={backgroundColor} color={color} size="xl" radius="xl">
|
||||||
|
{icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HealthSummaryData {
|
||||||
|
ibuHamil: number;
|
||||||
|
balita: number;
|
||||||
|
alertStunting: number;
|
||||||
|
posyanduAktif: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryCardsProps {
|
||||||
|
data?: HealthSummaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SummaryCards = ({ data }: SummaryCardsProps) => {
|
||||||
|
const defaultData: HealthSummaryData = {
|
||||||
|
ibuHamil: 87,
|
||||||
|
balita: 342,
|
||||||
|
alertStunting: 12,
|
||||||
|
posyanduAktif: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter="md">
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Ibu Hamil Aktif"
|
||||||
|
value={displayData.ibuHamil}
|
||||||
|
subtitle="Aktif"
|
||||||
|
icon={<IconHeartbeat size={20} />}
|
||||||
|
color= "white"
|
||||||
|
backgroundColor= "#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Balita Terdaftar"
|
||||||
|
value={displayData.balita}
|
||||||
|
subtitle="Terdaftar"
|
||||||
|
icon={<IconBabyCarriage size={20} />}
|
||||||
|
color= "white"
|
||||||
|
backgroundColor= "#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Alert Stunting"
|
||||||
|
value={displayData.alertStunting}
|
||||||
|
subtitle="Perhatian"
|
||||||
|
icon={<IconStethoscope size={20} />}
|
||||||
|
color= "white"
|
||||||
|
backgroundColor= "#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Posyandu Aktif"
|
||||||
|
value={displayData.posyanduAktif}
|
||||||
|
subtitle="Aktif"
|
||||||
|
icon={<IconMedicalCross size={20} />}
|
||||||
|
color= "white"
|
||||||
|
backgroundColor= "#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -52,8 +52,8 @@ export const HelpCard = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isDark
|
backgroundColor: isDark
|
||||||
? theme.colors.blue[8]
|
? "#263852ff"
|
||||||
: theme.colors.blue[0],
|
: "#1E3A5F",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
BIN
src/components/ui/logo-desa-plus.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
75
src/components/umkm/header-toggle.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import { setRange, umkmStore } from "../../store/umkm";
|
||||||
|
|
||||||
|
type TimeRange = "minggu" | "bulan";
|
||||||
|
|
||||||
|
interface HeaderToggleProps {
|
||||||
|
title?: string;
|
||||||
|
onRangeChange?: (range: TimeRange) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderToggle = ({
|
||||||
|
title = "Update Penjualan Produk",
|
||||||
|
onRangeChange,
|
||||||
|
}: HeaderToggleProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
const { selectedRange } = useSnapshot(umkmStore);
|
||||||
|
|
||||||
|
const handleRangeChange = (range: TimeRange) => {
|
||||||
|
setRange(range);
|
||||||
|
onRangeChange?.(range);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#1e3a5f" : "#1e3a5f"}
|
||||||
|
style={{ borderColor: dark ? "#1e3a5f" : "#1e3a5f" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" px="md" py="xs">
|
||||||
|
<Title order={3} c="white">
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
variant={selectedRange === "minggu" ? "white" : "transparent"}
|
||||||
|
onClick={() => handleRangeChange("minggu")}
|
||||||
|
c={selectedRange === "minggu" ? "#1e3a5f" : "white"}
|
||||||
|
fw={600}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
opacity: selectedRange === "minggu" ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Minggu ini
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={selectedRange === "bulan" ? "white" : "transparent"}
|
||||||
|
onClick={() => handleRangeChange("bulan")}
|
||||||
|
c={selectedRange === "bulan" ? "#1e3a5f" : "white"}
|
||||||
|
fw={600}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
opacity: selectedRange === "bulan" ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bulan ini
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
100
src/components/umkm/produk-unggulan.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
trend?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricCard = ({ title, value, trend }: MetricCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Stack gap={0} align="flex-end">
|
||||||
|
<Text size="lg" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{trend && (
|
||||||
|
<Text size="xs" c={trend.value >= 0 ? "green" : "red"} fw={600}>
|
||||||
|
{trend.value >= 0 ? "↑" : "↓"} {Math.abs(trend.value)}%{" "}
|
||||||
|
{trend.label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProdukUnggulanProps {
|
||||||
|
data?: {
|
||||||
|
totalPenjualan: number;
|
||||||
|
produkAktif: number;
|
||||||
|
totalTransaksi: number;
|
||||||
|
trend?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProdukUnggulan = ({ data }: ProdukUnggulanProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
totalPenjualan: 30900000,
|
||||||
|
produkAktif: 7,
|
||||||
|
totalTransaksi: 500,
|
||||||
|
trend: {
|
||||||
|
value: 18,
|
||||||
|
label: "vs bulan lalu",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return `Rp ${value.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Penjualan"
|
||||||
|
value={formatCurrency(displayData.totalPenjualan)}
|
||||||
|
trend={displayData.trend}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Produk Aktif"
|
||||||
|
value={`${displayData.produkAktif} kategori`}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Total Transaksi"
|
||||||
|
value={`${displayData.totalTransaksi} transaksi`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
260
src/components/umkm/sales-table.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export interface SalesData {
|
||||||
|
id: string;
|
||||||
|
produk: string;
|
||||||
|
penjualanBulanIni: number;
|
||||||
|
bulanLalu: number;
|
||||||
|
trend: number;
|
||||||
|
volume: string;
|
||||||
|
stok: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SalesTableProps {
|
||||||
|
data?: SalesData[];
|
||||||
|
onDetailClick?: (product: SalesData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SalesTable = ({ data, onDetailClick }: SalesTableProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: SalesData[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
produk: "Beras Premium Organik",
|
||||||
|
penjualanBulanIni: 8500000,
|
||||||
|
bulanLalu: 7600000,
|
||||||
|
trend: 12,
|
||||||
|
volume: "650 Kg",
|
||||||
|
stok: 850,
|
||||||
|
unit: "Kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
produk: "Keripik Singkong",
|
||||||
|
penjualanBulanIni: 4200000,
|
||||||
|
bulanLalu: 3800000,
|
||||||
|
trend: 11,
|
||||||
|
volume: "320 Kg",
|
||||||
|
stok: 120,
|
||||||
|
unit: "Kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
produk: "Madu Alami",
|
||||||
|
penjualanBulanIni: 3750000,
|
||||||
|
bulanLalu: 4100000,
|
||||||
|
trend: -9,
|
||||||
|
volume: "150 Liter",
|
||||||
|
stok: 45,
|
||||||
|
unit: "Liter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
produk: "Kecap Tradisional",
|
||||||
|
penjualanBulanIni: 2800000,
|
||||||
|
bulanLalu: 2500000,
|
||||||
|
trend: 12,
|
||||||
|
volume: "280 Botol",
|
||||||
|
stok: 95,
|
||||||
|
unit: "Botol",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
produk: "Sambal Bu Rudy",
|
||||||
|
penjualanBulanIni: 2100000,
|
||||||
|
bulanLalu: 2300000,
|
||||||
|
trend: -9,
|
||||||
|
volume: "180 Botol",
|
||||||
|
stok: 35,
|
||||||
|
unit: "Botol",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return `Rp ${value.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStockStatus = (stock: number) => {
|
||||||
|
if (stock > 200) return { color: "green", label: "Aman" };
|
||||||
|
if (stock > 50) return { color: "yellow", label: "Sedang" };
|
||||||
|
return { color: "red", label: "Rendah" };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Detail Penjualan Produk
|
||||||
|
</Title>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Select
|
||||||
|
placeholder="Filter kategori"
|
||||||
|
data={[
|
||||||
|
{ value: "semua", label: "Semua Kategori" },
|
||||||
|
{ value: "makanan", label: "Makanan" },
|
||||||
|
{ value: "minuman", label: "Minuman" },
|
||||||
|
{ value: "kerajinan", label: "Kerajinan" },
|
||||||
|
]}
|
||||||
|
defaultValue="semua"
|
||||||
|
w={180}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Filter UMKM"
|
||||||
|
data={[
|
||||||
|
{ value: "semua", label: "Semua UMKM" },
|
||||||
|
{ value: "umkm1", label: "Warung Pak Joko" },
|
||||||
|
{ value: "umkm2", label: "Ibu Sari Snack" },
|
||||||
|
]}
|
||||||
|
defaultValue="semua"
|
||||||
|
w={180}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
stickyHeaderOffset={60}
|
||||||
|
highlightOnHover
|
||||||
|
withRowBorders={false}
|
||||||
|
verticalSpacing="sm"
|
||||||
|
>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Produk
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Penjualan Bulan Ini
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Bulan Lalu
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Trend
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Volume
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Stok
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Aksi
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{displayData.map((product) => {
|
||||||
|
const stockStatus = getStockStatus(product.stok);
|
||||||
|
return (
|
||||||
|
<Table.Tr
|
||||||
|
key={product.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: dark ? "#141D34" : "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.produk}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{formatCurrency(product.penjualanBulanIni)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{formatCurrency(product.bulanLalu)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs">
|
||||||
|
{product.trend >= 0 ? (
|
||||||
|
<IconArrowUp size={16} color="green" />
|
||||||
|
) : (
|
||||||
|
<IconArrowDown size={16} color="red" />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
c={product.trend >= 0 ? "green" : "red"}
|
||||||
|
>
|
||||||
|
{Math.abs(product.trend)}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.volume}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge variant="light" color={stockStatus.color} size="sm">
|
||||||
|
{product.stok} {product.unit} ({stockStatus.label})
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="compact-sm"
|
||||||
|
color="darmasaba-blue"
|
||||||
|
radius="xl"
|
||||||
|
onClick={() => onDetailClick?.(product)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
src/components/umkm/summary-cards.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconCategory,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconTrendingUp,
|
||||||
|
IconUsers
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface KpiCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KpiCard = ({ title, value, subtitle, icon, color, backgroundColor }: KpiCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const formatValue = (val: string | number) => {
|
||||||
|
if (typeof val === "number") {
|
||||||
|
if (val >= 1000000) {
|
||||||
|
return `${(val / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (val >= 1000) {
|
||||||
|
return `${(val / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return val.toLocaleString();
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{formatValue(value)}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Avatar
|
||||||
|
color={color}
|
||||||
|
bg={backgroundColor}
|
||||||
|
size={40}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Avatar>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SummaryCardsProps {
|
||||||
|
data?: {
|
||||||
|
umkmAktif: number;
|
||||||
|
umkmTerdaftar: number;
|
||||||
|
omzet: number;
|
||||||
|
kategoriTerbanyak: { count: number; name: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SummaryCards = ({ data }: SummaryCardsProps) => {
|
||||||
|
const defaultData = {
|
||||||
|
umkmAktif: 45,
|
||||||
|
umkmTerdaftar: 68,
|
||||||
|
omzet: 48000000,
|
||||||
|
kategoriTerbanyak: { count: 34, name: "Kuliner" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const kpiData: KpiCardProps[] = [
|
||||||
|
{
|
||||||
|
title: "UMKM Aktif",
|
||||||
|
value: displayData.umkmAktif,
|
||||||
|
subtitle: "Beroperasi",
|
||||||
|
icon: <IconCurrencyDollar size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "UMKM Terdaftar",
|
||||||
|
value: displayData.umkmTerdaftar,
|
||||||
|
subtitle: "Total registrasi",
|
||||||
|
icon: <IconUsers size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Omzet",
|
||||||
|
value: displayData.omzet,
|
||||||
|
subtitle: "Omzet BUMDes per bulan",
|
||||||
|
icon: <IconTrendingUp size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "UMKM Terbanyak",
|
||||||
|
value: displayData.kategoriTerbanyak.count,
|
||||||
|
subtitle: `Kategori ${displayData.kategoriTerbanyak.name}`,
|
||||||
|
icon: <IconTrendingUp size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((kpi, index) => (
|
||||||
|
<GridCol key={index} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<KpiCard {...kpi} />
|
||||||
|
</GridCol>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
140
src/components/umkm/top-products.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface TopProduct {
|
||||||
|
rank: number;
|
||||||
|
name: string;
|
||||||
|
umkmName: string;
|
||||||
|
revenue: number;
|
||||||
|
quantitySold: number;
|
||||||
|
trend: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopProductsProps {
|
||||||
|
products?: TopProduct[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (value: number) => {
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TopProducts = ({ products }: TopProductsProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultProducts: TopProduct[] = [
|
||||||
|
{
|
||||||
|
rank: 1,
|
||||||
|
name: "Beras Premium Organik",
|
||||||
|
umkmName: "Warung Pak Joko",
|
||||||
|
revenue: 8500000,
|
||||||
|
quantitySold: 650,
|
||||||
|
trend: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 2,
|
||||||
|
name: "Keripik Singkong",
|
||||||
|
umkmName: "Ibu Sari Snack",
|
||||||
|
revenue: 4200000,
|
||||||
|
quantitySold: 320,
|
||||||
|
trend: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 3,
|
||||||
|
name: "Madu Alami",
|
||||||
|
umkmName: "Peternakan Lebah",
|
||||||
|
revenue: 3750000,
|
||||||
|
quantitySold: 150,
|
||||||
|
trend: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayProducts = products || defaultProducts;
|
||||||
|
|
||||||
|
const getRankColor = (rank: number) => {
|
||||||
|
if (rank === 1) return "yellow";
|
||||||
|
if (rank === 2) return "gray";
|
||||||
|
if (rank === 3) return "orange";
|
||||||
|
return "blue";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={4} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Top 3 Produk Terlaris
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayProducts.map((product) => (
|
||||||
|
<Group key={product.rank} justify="space-between" align="center">
|
||||||
|
<Group gap="sm">
|
||||||
|
<Badge
|
||||||
|
variant="filled"
|
||||||
|
color={getRankColor(product.rank)}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
w={30}
|
||||||
|
h={30}
|
||||||
|
>
|
||||||
|
{product.rank}
|
||||||
|
</Badge>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{product.umkmName}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs" mt={2}>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
Rp {formatCurrency(product.revenue)}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{formatNumber(product.quantitySold)} terjual
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color={product.trend >= 0 ? "green" : "red"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{product.trend >= 0 ? "+" : ""}
|
||||||
|
{product.trend}%
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
src/hooks/use-sidebar-fullscreen.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
|
||||||
|
export function useSidebarFullscreen() {
|
||||||
|
const [opened, { toggle: toggleMobile }] = useDisclosure();
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setSidebarCollapsed.toggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMainClick = () => {
|
||||||
|
if (!sidebarCollapsed) {
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
isCollapsed: sidebarCollapsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/index.ts
@@ -10,6 +10,18 @@ const PORT = process.env.PORT || 3000;
|
|||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
// Auto-seed database in production (ensure admin user exists)
|
||||||
|
if (isProduction && process.env.ADMIN_EMAIL) {
|
||||||
|
try {
|
||||||
|
console.log("🌱 Running database seed in production...");
|
||||||
|
const { runSeed } = await import("../prisma/seed.ts");
|
||||||
|
await runSeed();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("⚠️ Production seed failed:", error);
|
||||||
|
// Don't crash the server if seed fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const app = new Elysia().use(api);
|
const app = new Elysia().use(api);
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
@@ -83,10 +95,25 @@ if (!isProduction) {
|
|||||||
getHeader(name: string) {
|
getHeader(name: string) {
|
||||||
return this.headers[name.toLowerCase()];
|
return this.headers[name.toLowerCase()];
|
||||||
},
|
},
|
||||||
|
writeHead(code: number, headers: Record<string, string>) {
|
||||||
|
this.statusCode = code;
|
||||||
|
Object.assign(this.headers, headers);
|
||||||
|
},
|
||||||
|
write(chunk: any, callback?: () => void) {
|
||||||
|
// Collect chunks for streaming responses
|
||||||
|
if (!this._chunks) this._chunks = [];
|
||||||
|
this._chunks.push(chunk);
|
||||||
|
if (callback) callback();
|
||||||
|
return true; // Indicate we can accept more data
|
||||||
|
},
|
||||||
headers: {} as Record<string, string>,
|
headers: {} as Record<string, string>,
|
||||||
end(data: any) {
|
end(data: any) {
|
||||||
// Handle potential Buffer or string data from Vite
|
// Handle potential Buffer or string data from Vite
|
||||||
let body = data;
|
let body = data;
|
||||||
|
// If we have collected chunks from write() calls, combine them
|
||||||
|
if (this._chunks && this._chunks.length > 0) {
|
||||||
|
body = Buffer.concat(this._chunks);
|
||||||
|
}
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
body = data;
|
body = data;
|
||||||
} else if (typeof data === "string") {
|
} else if (typeof data === "string") {
|
||||||
@@ -146,6 +173,11 @@ if (!isProduction) {
|
|||||||
if (fs.existsSync(srcPath)) {
|
if (fs.existsSync(srcPath)) {
|
||||||
filePath = srcPath;
|
filePath = srcPath;
|
||||||
}
|
}
|
||||||
|
// Check public folder for static assets
|
||||||
|
const publicPath = path.join("public", pathname);
|
||||||
|
if (fs.existsSync(publicPath)) {
|
||||||
|
filePath = publicPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
||||||
@@ -161,8 +193,18 @@ if (!isProduction) {
|
|||||||
) {
|
) {
|
||||||
filePath = fallbackDistPath;
|
filePath = fallbackDistPath;
|
||||||
}
|
}
|
||||||
|
// Try public folder
|
||||||
|
else {
|
||||||
|
const fallbackPublicPath = path.join("public", filename);
|
||||||
|
if (
|
||||||
|
fs.existsSync(fallbackPublicPath) &&
|
||||||
|
fs.statSync(fallbackPublicPath).isFile()
|
||||||
|
) {
|
||||||
|
filePath = fallbackPublicPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Special handling for PWA files in src
|
// Special handling for PWA files in src
|
||||||
else if (pathname.includes("assetlinks.json")) {
|
if (pathname.includes("assetlinks.json")) {
|
||||||
const srcFilename = pathname.includes("assetlinks.json")
|
const srcFilename = pathname.includes("assetlinks.json")
|
||||||
? ".well-known/assetlinks.json"
|
? ".well-known/assetlinks.json"
|
||||||
: filename;
|
: filename;
|
||||||
@@ -207,4 +249,3 @@ console.log(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type ApiApp = typeof app;
|
export type ApiApp = typeof app;
|
||||||
|
|
||||||
|
|||||||
@@ -60,16 +60,39 @@ type RouteRule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routeRules: RouteRule[] = [
|
const routeRules: RouteRule[] = [
|
||||||
|
// Public routes - no auth required
|
||||||
|
{
|
||||||
|
match: (p) => p === "/" || p === "/signin" || p === "/signup",
|
||||||
|
requireAuth: false,
|
||||||
|
},
|
||||||
|
// Profile routes - auth required for all roles
|
||||||
{
|
{
|
||||||
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
redirectTo: "/signin",
|
redirectTo: "/signin",
|
||||||
},
|
},
|
||||||
|
// Dashboard and main pages - auth required for all roles (not just admin)
|
||||||
{
|
{
|
||||||
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"),
|
match: (p) =>
|
||||||
|
p.startsWith("/kinerja-divisi") ||
|
||||||
|
p.startsWith("/pengaduan") ||
|
||||||
|
p.startsWith("/jenna") ||
|
||||||
|
p.startsWith("/demografi") ||
|
||||||
|
p.startsWith("/keuangan") ||
|
||||||
|
p.startsWith("/bumdes") ||
|
||||||
|
p.startsWith("/sosial") ||
|
||||||
|
p.startsWith("/keamanan") ||
|
||||||
|
p.startsWith("/bantuan") ||
|
||||||
|
p.startsWith("/pengaturan"),
|
||||||
|
requireAuth: true,
|
||||||
|
redirectTo: "/signin",
|
||||||
|
},
|
||||||
|
// Admin routes - auth required with admin role only
|
||||||
|
{
|
||||||
|
match: (p) => p.startsWith("/admin"),
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
requiredRole: "admin",
|
requiredRole: "admin",
|
||||||
redirectTo: "/profile",
|
redirectTo: "/signin",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -98,15 +121,22 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
|||||||
location: { pathname: string; href: string };
|
location: { pathname: string; href: string };
|
||||||
}) => {
|
}) => {
|
||||||
const rule = findRouteRule(location.pathname);
|
const rule = findRouteRule(location.pathname);
|
||||||
|
|
||||||
|
// If no rule matches, allow access by default
|
||||||
if (!rule) return;
|
if (!rule) return;
|
||||||
|
|
||||||
|
// If route explicitly doesn't require auth, allow access
|
||||||
|
if (rule.requireAuth === false) return;
|
||||||
|
|
||||||
const session = await fetchSession();
|
const session = await fetchSession();
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
|
|
||||||
|
// If auth is required but user is not logged in, redirect to login
|
||||||
if (rule.requireAuth && !user) {
|
if (rule.requireAuth && !user) {
|
||||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If specific role is required, check it
|
||||||
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
||||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,38 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as SosialRouteImport } from './routes/sosial'
|
||||||
import { Route as SignupRouteImport } from './routes/signup'
|
import { Route as SignupRouteImport } from './routes/signup'
|
||||||
import { Route as SigninRouteImport } from './routes/signin'
|
import { Route as SigninRouteImport } from './routes/signin'
|
||||||
import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
|
import { Route as PengaduanLayananPublikRouteImport } from './routes/pengaduan-layanan-publik'
|
||||||
|
import { Route as KinerjaDivisiRouteImport } from './routes/kinerja-divisi'
|
||||||
|
import { Route as KeuanganAnggaranRouteImport } from './routes/keuangan-anggaran'
|
||||||
|
import { Route as KeamananRouteImport } from './routes/keamanan'
|
||||||
|
import { Route as JennaAnalyticRouteImport } from './routes/jenna-analytic'
|
||||||
|
import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan'
|
||||||
|
import { Route as BumdesRouteImport } from './routes/bumdes'
|
||||||
|
import { Route as BantuanRouteImport } from './routes/bantuan'
|
||||||
|
import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route'
|
||||||
import { Route as AdminRouteRouteImport } from './routes/admin/route'
|
import { Route as AdminRouteRouteImport } from './routes/admin/route'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
||||||
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
|
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
|
||||||
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
|
|
||||||
import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
||||||
import { Route as UsersIdRouteImport } from './routes/users/$id'
|
import { Route as UsersIdRouteImport } from './routes/users/$id'
|
||||||
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
|
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
|
||||||
import { Route as DashboardSosialRouteImport } from './routes/dashboard/sosial'
|
import { Route as PengaturanUmumRouteImport } from './routes/pengaturan/umum'
|
||||||
import { Route as DashboardPengaduanLayananPublikRouteImport } from './routes/dashboard/pengaduan-layanan-publik'
|
import { Route as PengaturanNotifikasiRouteImport } from './routes/pengaturan/notifikasi'
|
||||||
import { Route as DashboardKinerjaDivisiRouteImport } from './routes/dashboard/kinerja-divisi'
|
import { Route as PengaturanKeamananRouteImport } from './routes/pengaturan/keamanan'
|
||||||
import { Route as DashboardKeuanganAnggaranRouteImport } from './routes/dashboard/keuangan-anggaran'
|
import { Route as PengaturanAksesDanTimRouteImport } from './routes/pengaturan/akses-dan-tim'
|
||||||
import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keamanan'
|
|
||||||
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
|
|
||||||
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
|
|
||||||
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
|
|
||||||
import { Route as DashboardBantuanRouteImport } from './routes/dashboard/bantuan'
|
|
||||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||||
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
||||||
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
|
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
|
||||||
import { Route as DashboardPengaturanRouteRouteImport } from './routes/dashboard/pengaturan/route'
|
|
||||||
import { Route as DashboardPengaturanUmumRouteImport } from './routes/dashboard/pengaturan/umum'
|
|
||||||
import { Route as DashboardPengaturanNotifikasiRouteImport } from './routes/dashboard/pengaturan/notifikasi'
|
|
||||||
import { Route as DashboardPengaturanKeamananRouteImport } from './routes/dashboard/pengaturan/keamanan'
|
|
||||||
import { Route as DashboardPengaturanAksesDanTimRouteImport } from './routes/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
|
|
||||||
|
const SosialRoute = SosialRouteImport.update({
|
||||||
|
id: '/sosial',
|
||||||
|
path: '/sosial',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const SignupRoute = SignupRouteImport.update({
|
const SignupRoute = SignupRouteImport.update({
|
||||||
id: '/signup',
|
id: '/signup',
|
||||||
path: '/signup',
|
path: '/signup',
|
||||||
@@ -48,9 +51,49 @@ const SigninRoute = SigninRouteImport.update({
|
|||||||
path: '/signin',
|
path: '/signin',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardRouteRoute = DashboardRouteRouteImport.update({
|
const PengaduanLayananPublikRoute = PengaduanLayananPublikRouteImport.update({
|
||||||
id: '/dashboard',
|
id: '/pengaduan-layanan-publik',
|
||||||
path: '/dashboard',
|
path: '/pengaduan-layanan-publik',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const KinerjaDivisiRoute = KinerjaDivisiRouteImport.update({
|
||||||
|
id: '/kinerja-divisi',
|
||||||
|
path: '/kinerja-divisi',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const KeuanganAnggaranRoute = KeuanganAnggaranRouteImport.update({
|
||||||
|
id: '/keuangan-anggaran',
|
||||||
|
path: '/keuangan-anggaran',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const KeamananRoute = KeamananRouteImport.update({
|
||||||
|
id: '/keamanan',
|
||||||
|
path: '/keamanan',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const JennaAnalyticRoute = JennaAnalyticRouteImport.update({
|
||||||
|
id: '/jenna-analytic',
|
||||||
|
path: '/jenna-analytic',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const DemografiPekerjaanRoute = DemografiPekerjaanRouteImport.update({
|
||||||
|
id: '/demografi-pekerjaan',
|
||||||
|
path: '/demografi-pekerjaan',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const BumdesRoute = BumdesRouteImport.update({
|
||||||
|
id: '/bumdes',
|
||||||
|
path: '/bumdes',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const BantuanRoute = BantuanRouteImport.update({
|
||||||
|
id: '/bantuan',
|
||||||
|
path: '/bantuan',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PengaturanRouteRoute = PengaturanRouteRouteImport.update({
|
||||||
|
id: '/pengaturan',
|
||||||
|
path: '/pengaturan',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AdminRouteRoute = AdminRouteRouteImport.update({
|
const AdminRouteRoute = AdminRouteRouteImport.update({
|
||||||
@@ -73,11 +116,6 @@ const ProfileIndexRoute = ProfileIndexRouteImport.update({
|
|||||||
path: '/profile/',
|
path: '/profile/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardIndexRoute = DashboardIndexRouteImport.update({
|
|
||||||
id: '/',
|
|
||||||
path: '/',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const AdminIndexRoute = AdminIndexRouteImport.update({
|
const AdminIndexRoute = AdminIndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -93,53 +131,25 @@ const ProfileEditRoute = ProfileEditRouteImport.update({
|
|||||||
path: '/profile/edit',
|
path: '/profile/edit',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardSosialRoute = DashboardSosialRouteImport.update({
|
const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
|
||||||
id: '/sosial',
|
id: '/umum',
|
||||||
path: '/sosial',
|
path: '/umum',
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
getParentRoute: () => PengaturanRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardPengaduanLayananPublikRoute =
|
const PengaturanNotifikasiRoute = PengaturanNotifikasiRouteImport.update({
|
||||||
DashboardPengaduanLayananPublikRouteImport.update({
|
id: '/notifikasi',
|
||||||
id: '/pengaduan-layanan-publik',
|
path: '/notifikasi',
|
||||||
path: '/pengaduan-layanan-publik',
|
getParentRoute: () => PengaturanRouteRoute,
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardKinerjaDivisiRoute = DashboardKinerjaDivisiRouteImport.update({
|
|
||||||
id: '/kinerja-divisi',
|
|
||||||
path: '/kinerja-divisi',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardKeuanganAnggaranRoute =
|
const PengaturanKeamananRoute = PengaturanKeamananRouteImport.update({
|
||||||
DashboardKeuanganAnggaranRouteImport.update({
|
|
||||||
id: '/keuangan-anggaran',
|
|
||||||
path: '/keuangan-anggaran',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardKeamananRoute = DashboardKeamananRouteImport.update({
|
|
||||||
id: '/keamanan',
|
id: '/keamanan',
|
||||||
path: '/keamanan',
|
path: '/keamanan',
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
getParentRoute: () => PengaturanRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardJennaAnalyticRoute = DashboardJennaAnalyticRouteImport.update({
|
const PengaturanAksesDanTimRoute = PengaturanAksesDanTimRouteImport.update({
|
||||||
id: '/jenna-analytic',
|
id: '/akses-dan-tim',
|
||||||
path: '/jenna-analytic',
|
path: '/akses-dan-tim',
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
getParentRoute: () => PengaturanRouteRoute,
|
||||||
} as any)
|
|
||||||
const DashboardDemografiPekerjaanRoute =
|
|
||||||
DashboardDemografiPekerjaanRouteImport.update({
|
|
||||||
id: '/demografi-pekerjaan',
|
|
||||||
path: '/demografi-pekerjaan',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
|
|
||||||
id: '/bumdes',
|
|
||||||
path: '/bumdes',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardBantuanRoute = DashboardBantuanRouteImport.update({
|
|
||||||
id: '/bantuan',
|
|
||||||
path: '/bantuan',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
const AdminUsersRoute = AdminUsersRouteImport.update({
|
const AdminUsersRoute = AdminUsersRouteImport.update({
|
||||||
id: '/users',
|
id: '/users',
|
||||||
@@ -156,222 +166,192 @@ const AdminApikeyRoute = AdminApikeyRouteImport.update({
|
|||||||
path: '/apikey',
|
path: '/apikey',
|
||||||
getParentRoute: () => AdminRouteRoute,
|
getParentRoute: () => AdminRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const DashboardPengaturanRouteRoute =
|
|
||||||
DashboardPengaturanRouteRouteImport.update({
|
|
||||||
id: '/pengaturan',
|
|
||||||
path: '/pengaturan',
|
|
||||||
getParentRoute: () => DashboardRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardPengaturanUmumRoute = DashboardPengaturanUmumRouteImport.update({
|
|
||||||
id: '/umum',
|
|
||||||
path: '/umum',
|
|
||||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardPengaturanNotifikasiRoute =
|
|
||||||
DashboardPengaturanNotifikasiRouteImport.update({
|
|
||||||
id: '/notifikasi',
|
|
||||||
path: '/notifikasi',
|
|
||||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardPengaturanKeamananRoute =
|
|
||||||
DashboardPengaturanKeamananRouteImport.update({
|
|
||||||
id: '/keamanan',
|
|
||||||
path: '/keamanan',
|
|
||||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
|
||||||
} as any)
|
|
||||||
const DashboardPengaturanAksesDanTimRoute =
|
|
||||||
DashboardPengaturanAksesDanTimRouteImport.update({
|
|
||||||
id: '/akses-dan-tim',
|
|
||||||
path: '/akses-dan-tim',
|
|
||||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteRouteWithChildren
|
'/admin': typeof AdminRouteRouteWithChildren
|
||||||
'/dashboard': typeof DashboardRouteRouteWithChildren
|
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||||
|
'/bantuan': typeof BantuanRoute
|
||||||
|
'/bumdes': typeof BumdesRoute
|
||||||
|
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||||
|
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||||
|
'/keamanan': typeof KeamananRoute
|
||||||
|
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||||
|
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||||
|
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
'/sosial': typeof SosialRoute
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
|
||||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
|
||||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
|
||||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
|
||||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin/': typeof AdminIndexRoute
|
'/admin/': typeof AdminIndexRoute
|
||||||
'/dashboard/': typeof DashboardIndexRoute
|
|
||||||
'/profile/': typeof ProfileIndexRoute
|
'/profile/': typeof ProfileIndexRoute
|
||||||
'/users/': typeof UsersIndexRoute
|
'/users/': typeof UsersIndexRoute
|
||||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
|
||||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
|
||||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
|
||||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||||
|
'/bantuan': typeof BantuanRoute
|
||||||
|
'/bumdes': typeof BumdesRoute
|
||||||
|
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||||
|
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||||
|
'/keamanan': typeof KeamananRoute
|
||||||
|
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||||
|
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||||
|
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
'/sosial': typeof SosialRoute
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
|
||||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
|
||||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
|
||||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
|
||||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin': typeof AdminIndexRoute
|
'/admin': typeof AdminIndexRoute
|
||||||
'/dashboard': typeof DashboardIndexRoute
|
|
||||||
'/profile': typeof ProfileIndexRoute
|
'/profile': typeof ProfileIndexRoute
|
||||||
'/users': typeof UsersIndexRoute
|
'/users': typeof UsersIndexRoute
|
||||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
|
||||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
|
||||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
|
||||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteRouteWithChildren
|
'/admin': typeof AdminRouteRouteWithChildren
|
||||||
'/dashboard': typeof DashboardRouteRouteWithChildren
|
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||||
|
'/bantuan': typeof BantuanRoute
|
||||||
|
'/bumdes': typeof BumdesRoute
|
||||||
|
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||||
|
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||||
|
'/keamanan': typeof KeamananRoute
|
||||||
|
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||||
|
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||||
|
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
'/sosial': typeof SosialRoute
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
|
||||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
|
||||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
|
||||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
|
||||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin/': typeof AdminIndexRoute
|
'/admin/': typeof AdminIndexRoute
|
||||||
'/dashboard/': typeof DashboardIndexRoute
|
|
||||||
'/profile/': typeof ProfileIndexRoute
|
'/profile/': typeof ProfileIndexRoute
|
||||||
'/users/': typeof UsersIndexRoute
|
'/users/': typeof UsersIndexRoute
|
||||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
|
||||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
|
||||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
|
||||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/dashboard'
|
| '/pengaturan'
|
||||||
|
| '/bantuan'
|
||||||
|
| '/bumdes'
|
||||||
|
| '/demografi-pekerjaan'
|
||||||
|
| '/jenna-analytic'
|
||||||
|
| '/keamanan'
|
||||||
|
| '/keuangan-anggaran'
|
||||||
|
| '/kinerja-divisi'
|
||||||
|
| '/pengaduan-layanan-publik'
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/dashboard/pengaturan'
|
| '/sosial'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/dashboard/bantuan'
|
| '/pengaturan/akses-dan-tim'
|
||||||
| '/dashboard/bumdes'
|
| '/pengaturan/keamanan'
|
||||||
| '/dashboard/demografi-pekerjaan'
|
| '/pengaturan/notifikasi'
|
||||||
| '/dashboard/jenna-analytic'
|
| '/pengaturan/umum'
|
||||||
| '/dashboard/keamanan'
|
|
||||||
| '/dashboard/keuangan-anggaran'
|
|
||||||
| '/dashboard/kinerja-divisi'
|
|
||||||
| '/dashboard/pengaduan-layanan-publik'
|
|
||||||
| '/dashboard/sosial'
|
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/dashboard/'
|
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
| '/users/'
|
| '/users/'
|
||||||
| '/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
| '/dashboard/pengaturan/keamanan'
|
|
||||||
| '/dashboard/pengaturan/notifikasi'
|
|
||||||
| '/dashboard/pengaturan/umum'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/pengaturan'
|
||||||
|
| '/bantuan'
|
||||||
|
| '/bumdes'
|
||||||
|
| '/demografi-pekerjaan'
|
||||||
|
| '/jenna-analytic'
|
||||||
|
| '/keamanan'
|
||||||
|
| '/keuangan-anggaran'
|
||||||
|
| '/kinerja-divisi'
|
||||||
|
| '/pengaduan-layanan-publik'
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/dashboard/pengaturan'
|
| '/sosial'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/dashboard/bantuan'
|
| '/pengaturan/akses-dan-tim'
|
||||||
| '/dashboard/bumdes'
|
| '/pengaturan/keamanan'
|
||||||
| '/dashboard/demografi-pekerjaan'
|
| '/pengaturan/notifikasi'
|
||||||
| '/dashboard/jenna-analytic'
|
| '/pengaturan/umum'
|
||||||
| '/dashboard/keamanan'
|
|
||||||
| '/dashboard/keuangan-anggaran'
|
|
||||||
| '/dashboard/kinerja-divisi'
|
|
||||||
| '/dashboard/pengaduan-layanan-publik'
|
|
||||||
| '/dashboard/sosial'
|
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/dashboard'
|
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/users'
|
| '/users'
|
||||||
| '/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
| '/dashboard/pengaturan/keamanan'
|
|
||||||
| '/dashboard/pengaturan/notifikasi'
|
|
||||||
| '/dashboard/pengaturan/umum'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/dashboard'
|
| '/pengaturan'
|
||||||
|
| '/bantuan'
|
||||||
|
| '/bumdes'
|
||||||
|
| '/demografi-pekerjaan'
|
||||||
|
| '/jenna-analytic'
|
||||||
|
| '/keamanan'
|
||||||
|
| '/keuangan-anggaran'
|
||||||
|
| '/kinerja-divisi'
|
||||||
|
| '/pengaduan-layanan-publik'
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/dashboard/pengaturan'
|
| '/sosial'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/dashboard/bantuan'
|
| '/pengaturan/akses-dan-tim'
|
||||||
| '/dashboard/bumdes'
|
| '/pengaturan/keamanan'
|
||||||
| '/dashboard/demografi-pekerjaan'
|
| '/pengaturan/notifikasi'
|
||||||
| '/dashboard/jenna-analytic'
|
| '/pengaturan/umum'
|
||||||
| '/dashboard/keamanan'
|
|
||||||
| '/dashboard/keuangan-anggaran'
|
|
||||||
| '/dashboard/kinerja-divisi'
|
|
||||||
| '/dashboard/pengaduan-layanan-publik'
|
|
||||||
| '/dashboard/sosial'
|
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/dashboard/'
|
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
| '/users/'
|
| '/users/'
|
||||||
| '/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
| '/dashboard/pengaturan/keamanan'
|
|
||||||
| '/dashboard/pengaturan/notifikasi'
|
|
||||||
| '/dashboard/pengaturan/umum'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AdminRouteRoute: typeof AdminRouteRouteWithChildren
|
AdminRouteRoute: typeof AdminRouteRouteWithChildren
|
||||||
DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
|
PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren
|
||||||
|
BantuanRoute: typeof BantuanRoute
|
||||||
|
BumdesRoute: typeof BumdesRoute
|
||||||
|
DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute
|
||||||
|
JennaAnalyticRoute: typeof JennaAnalyticRoute
|
||||||
|
KeamananRoute: typeof KeamananRoute
|
||||||
|
KeuanganAnggaranRoute: typeof KeuanganAnggaranRoute
|
||||||
|
KinerjaDivisiRoute: typeof KinerjaDivisiRoute
|
||||||
|
PengaduanLayananPublikRoute: typeof PengaduanLayananPublikRoute
|
||||||
SigninRoute: typeof SigninRoute
|
SigninRoute: typeof SigninRoute
|
||||||
SignupRoute: typeof SignupRoute
|
SignupRoute: typeof SignupRoute
|
||||||
|
SosialRoute: typeof SosialRoute
|
||||||
ProfileEditRoute: typeof ProfileEditRoute
|
ProfileEditRoute: typeof ProfileEditRoute
|
||||||
UsersIdRoute: typeof UsersIdRoute
|
UsersIdRoute: typeof UsersIdRoute
|
||||||
ProfileIndexRoute: typeof ProfileIndexRoute
|
ProfileIndexRoute: typeof ProfileIndexRoute
|
||||||
@@ -380,6 +360,13 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/sosial': {
|
||||||
|
id: '/sosial'
|
||||||
|
path: '/sosial'
|
||||||
|
fullPath: '/sosial'
|
||||||
|
preLoaderRoute: typeof SosialRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/signup': {
|
'/signup': {
|
||||||
id: '/signup'
|
id: '/signup'
|
||||||
path: '/signup'
|
path: '/signup'
|
||||||
@@ -394,11 +381,67 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SigninRouteImport
|
preLoaderRoute: typeof SigninRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/dashboard': {
|
'/pengaduan-layanan-publik': {
|
||||||
id: '/dashboard'
|
id: '/pengaduan-layanan-publik'
|
||||||
path: '/dashboard'
|
path: '/pengaduan-layanan-publik'
|
||||||
fullPath: '/dashboard'
|
fullPath: '/pengaduan-layanan-publik'
|
||||||
preLoaderRoute: typeof DashboardRouteRouteImport
|
preLoaderRoute: typeof PengaduanLayananPublikRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/kinerja-divisi': {
|
||||||
|
id: '/kinerja-divisi'
|
||||||
|
path: '/kinerja-divisi'
|
||||||
|
fullPath: '/kinerja-divisi'
|
||||||
|
preLoaderRoute: typeof KinerjaDivisiRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/keuangan-anggaran': {
|
||||||
|
id: '/keuangan-anggaran'
|
||||||
|
path: '/keuangan-anggaran'
|
||||||
|
fullPath: '/keuangan-anggaran'
|
||||||
|
preLoaderRoute: typeof KeuanganAnggaranRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/keamanan': {
|
||||||
|
id: '/keamanan'
|
||||||
|
path: '/keamanan'
|
||||||
|
fullPath: '/keamanan'
|
||||||
|
preLoaderRoute: typeof KeamananRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/jenna-analytic': {
|
||||||
|
id: '/jenna-analytic'
|
||||||
|
path: '/jenna-analytic'
|
||||||
|
fullPath: '/jenna-analytic'
|
||||||
|
preLoaderRoute: typeof JennaAnalyticRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/demografi-pekerjaan': {
|
||||||
|
id: '/demografi-pekerjaan'
|
||||||
|
path: '/demografi-pekerjaan'
|
||||||
|
fullPath: '/demografi-pekerjaan'
|
||||||
|
preLoaderRoute: typeof DemografiPekerjaanRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/bumdes': {
|
||||||
|
id: '/bumdes'
|
||||||
|
path: '/bumdes'
|
||||||
|
fullPath: '/bumdes'
|
||||||
|
preLoaderRoute: typeof BumdesRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/bantuan': {
|
||||||
|
id: '/bantuan'
|
||||||
|
path: '/bantuan'
|
||||||
|
fullPath: '/bantuan'
|
||||||
|
preLoaderRoute: typeof BantuanRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/pengaturan': {
|
||||||
|
id: '/pengaturan'
|
||||||
|
path: '/pengaturan'
|
||||||
|
fullPath: '/pengaturan'
|
||||||
|
preLoaderRoute: typeof PengaturanRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/admin': {
|
'/admin': {
|
||||||
@@ -429,13 +472,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileIndexRouteImport
|
preLoaderRoute: typeof ProfileIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/dashboard/': {
|
|
||||||
id: '/dashboard/'
|
|
||||||
path: '/'
|
|
||||||
fullPath: '/dashboard/'
|
|
||||||
preLoaderRoute: typeof DashboardIndexRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/admin/': {
|
'/admin/': {
|
||||||
id: '/admin/'
|
id: '/admin/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -457,68 +493,33 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileEditRouteImport
|
preLoaderRoute: typeof ProfileEditRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/dashboard/sosial': {
|
'/pengaturan/umum': {
|
||||||
id: '/dashboard/sosial'
|
id: '/pengaturan/umum'
|
||||||
path: '/sosial'
|
path: '/umum'
|
||||||
fullPath: '/dashboard/sosial'
|
fullPath: '/pengaturan/umum'
|
||||||
preLoaderRoute: typeof DashboardSosialRouteImport
|
preLoaderRoute: typeof PengaturanUmumRouteImport
|
||||||
parentRoute: typeof DashboardRouteRoute
|
parentRoute: typeof PengaturanRouteRoute
|
||||||
}
|
}
|
||||||
'/dashboard/pengaduan-layanan-publik': {
|
'/pengaturan/notifikasi': {
|
||||||
id: '/dashboard/pengaduan-layanan-publik'
|
id: '/pengaturan/notifikasi'
|
||||||
path: '/pengaduan-layanan-publik'
|
path: '/notifikasi'
|
||||||
fullPath: '/dashboard/pengaduan-layanan-publik'
|
fullPath: '/pengaturan/notifikasi'
|
||||||
preLoaderRoute: typeof DashboardPengaduanLayananPublikRouteImport
|
preLoaderRoute: typeof PengaturanNotifikasiRouteImport
|
||||||
parentRoute: typeof DashboardRouteRoute
|
parentRoute: typeof PengaturanRouteRoute
|
||||||
}
|
}
|
||||||
'/dashboard/kinerja-divisi': {
|
'/pengaturan/keamanan': {
|
||||||
id: '/dashboard/kinerja-divisi'
|
id: '/pengaturan/keamanan'
|
||||||
path: '/kinerja-divisi'
|
|
||||||
fullPath: '/dashboard/kinerja-divisi'
|
|
||||||
preLoaderRoute: typeof DashboardKinerjaDivisiRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/keuangan-anggaran': {
|
|
||||||
id: '/dashboard/keuangan-anggaran'
|
|
||||||
path: '/keuangan-anggaran'
|
|
||||||
fullPath: '/dashboard/keuangan-anggaran'
|
|
||||||
preLoaderRoute: typeof DashboardKeuanganAnggaranRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/keamanan': {
|
|
||||||
id: '/dashboard/keamanan'
|
|
||||||
path: '/keamanan'
|
path: '/keamanan'
|
||||||
fullPath: '/dashboard/keamanan'
|
fullPath: '/pengaturan/keamanan'
|
||||||
preLoaderRoute: typeof DashboardKeamananRouteImport
|
preLoaderRoute: typeof PengaturanKeamananRouteImport
|
||||||
parentRoute: typeof DashboardRouteRoute
|
parentRoute: typeof PengaturanRouteRoute
|
||||||
}
|
}
|
||||||
'/dashboard/jenna-analytic': {
|
'/pengaturan/akses-dan-tim': {
|
||||||
id: '/dashboard/jenna-analytic'
|
id: '/pengaturan/akses-dan-tim'
|
||||||
path: '/jenna-analytic'
|
path: '/akses-dan-tim'
|
||||||
fullPath: '/dashboard/jenna-analytic'
|
fullPath: '/pengaturan/akses-dan-tim'
|
||||||
preLoaderRoute: typeof DashboardJennaAnalyticRouteImport
|
preLoaderRoute: typeof PengaturanAksesDanTimRouteImport
|
||||||
parentRoute: typeof DashboardRouteRoute
|
parentRoute: typeof PengaturanRouteRoute
|
||||||
}
|
|
||||||
'/dashboard/demografi-pekerjaan': {
|
|
||||||
id: '/dashboard/demografi-pekerjaan'
|
|
||||||
path: '/demografi-pekerjaan'
|
|
||||||
fullPath: '/dashboard/demografi-pekerjaan'
|
|
||||||
preLoaderRoute: typeof DashboardDemografiPekerjaanRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/bumdes': {
|
|
||||||
id: '/dashboard/bumdes'
|
|
||||||
path: '/bumdes'
|
|
||||||
fullPath: '/dashboard/bumdes'
|
|
||||||
preLoaderRoute: typeof DashboardBumdesRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/bantuan': {
|
|
||||||
id: '/dashboard/bantuan'
|
|
||||||
path: '/bantuan'
|
|
||||||
fullPath: '/dashboard/bantuan'
|
|
||||||
preLoaderRoute: typeof DashboardBantuanRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
}
|
||||||
'/admin/users': {
|
'/admin/users': {
|
||||||
id: '/admin/users'
|
id: '/admin/users'
|
||||||
@@ -541,41 +542,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AdminApikeyRouteImport
|
preLoaderRoute: typeof AdminApikeyRouteImport
|
||||||
parentRoute: typeof AdminRouteRoute
|
parentRoute: typeof AdminRouteRoute
|
||||||
}
|
}
|
||||||
'/dashboard/pengaturan': {
|
|
||||||
id: '/dashboard/pengaturan'
|
|
||||||
path: '/pengaturan'
|
|
||||||
fullPath: '/dashboard/pengaturan'
|
|
||||||
preLoaderRoute: typeof DashboardPengaturanRouteRouteImport
|
|
||||||
parentRoute: typeof DashboardRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/pengaturan/umum': {
|
|
||||||
id: '/dashboard/pengaturan/umum'
|
|
||||||
path: '/umum'
|
|
||||||
fullPath: '/dashboard/pengaturan/umum'
|
|
||||||
preLoaderRoute: typeof DashboardPengaturanUmumRouteImport
|
|
||||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/pengaturan/notifikasi': {
|
|
||||||
id: '/dashboard/pengaturan/notifikasi'
|
|
||||||
path: '/notifikasi'
|
|
||||||
fullPath: '/dashboard/pengaturan/notifikasi'
|
|
||||||
preLoaderRoute: typeof DashboardPengaturanNotifikasiRouteImport
|
|
||||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/pengaturan/keamanan': {
|
|
||||||
id: '/dashboard/pengaturan/keamanan'
|
|
||||||
path: '/keamanan'
|
|
||||||
fullPath: '/dashboard/pengaturan/keamanan'
|
|
||||||
preLoaderRoute: typeof DashboardPengaturanKeamananRouteImport
|
|
||||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
|
||||||
}
|
|
||||||
'/dashboard/pengaturan/akses-dan-tim': {
|
|
||||||
id: '/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
path: '/akses-dan-tim'
|
|
||||||
fullPath: '/dashboard/pengaturan/akses-dan-tim'
|
|
||||||
preLoaderRoute: typeof DashboardPengaturanAksesDanTimRouteImport
|
|
||||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,64 +563,39 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
|
|||||||
AdminRouteRouteChildren,
|
AdminRouteRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface DashboardPengaturanRouteRouteChildren {
|
interface PengaturanRouteRouteChildren {
|
||||||
DashboardPengaturanAksesDanTimRoute: typeof DashboardPengaturanAksesDanTimRoute
|
PengaturanAksesDanTimRoute: typeof PengaturanAksesDanTimRoute
|
||||||
DashboardPengaturanKeamananRoute: typeof DashboardPengaturanKeamananRoute
|
PengaturanKeamananRoute: typeof PengaturanKeamananRoute
|
||||||
DashboardPengaturanNotifikasiRoute: typeof DashboardPengaturanNotifikasiRoute
|
PengaturanNotifikasiRoute: typeof PengaturanNotifikasiRoute
|
||||||
DashboardPengaturanUmumRoute: typeof DashboardPengaturanUmumRoute
|
PengaturanUmumRoute: typeof PengaturanUmumRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardPengaturanRouteRouteChildren: DashboardPengaturanRouteRouteChildren =
|
const PengaturanRouteRouteChildren: PengaturanRouteRouteChildren = {
|
||||||
{
|
PengaturanAksesDanTimRoute: PengaturanAksesDanTimRoute,
|
||||||
DashboardPengaturanAksesDanTimRoute: DashboardPengaturanAksesDanTimRoute,
|
PengaturanKeamananRoute: PengaturanKeamananRoute,
|
||||||
DashboardPengaturanKeamananRoute: DashboardPengaturanKeamananRoute,
|
PengaturanNotifikasiRoute: PengaturanNotifikasiRoute,
|
||||||
DashboardPengaturanNotifikasiRoute: DashboardPengaturanNotifikasiRoute,
|
PengaturanUmumRoute: PengaturanUmumRoute,
|
||||||
DashboardPengaturanUmumRoute: DashboardPengaturanUmumRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const DashboardPengaturanRouteRouteWithChildren =
|
|
||||||
DashboardPengaturanRouteRoute._addFileChildren(
|
|
||||||
DashboardPengaturanRouteRouteChildren,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface DashboardRouteRouteChildren {
|
|
||||||
DashboardPengaturanRouteRoute: typeof DashboardPengaturanRouteRouteWithChildren
|
|
||||||
DashboardBantuanRoute: typeof DashboardBantuanRoute
|
|
||||||
DashboardBumdesRoute: typeof DashboardBumdesRoute
|
|
||||||
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
|
|
||||||
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
|
|
||||||
DashboardKeamananRoute: typeof DashboardKeamananRoute
|
|
||||||
DashboardKeuanganAnggaranRoute: typeof DashboardKeuanganAnggaranRoute
|
|
||||||
DashboardKinerjaDivisiRoute: typeof DashboardKinerjaDivisiRoute
|
|
||||||
DashboardPengaduanLayananPublikRoute: typeof DashboardPengaduanLayananPublikRoute
|
|
||||||
DashboardSosialRoute: typeof DashboardSosialRoute
|
|
||||||
DashboardIndexRoute: typeof DashboardIndexRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
|
const PengaturanRouteRouteWithChildren = PengaturanRouteRoute._addFileChildren(
|
||||||
DashboardPengaturanRouteRoute: DashboardPengaturanRouteRouteWithChildren,
|
PengaturanRouteRouteChildren,
|
||||||
DashboardBantuanRoute: DashboardBantuanRoute,
|
|
||||||
DashboardBumdesRoute: DashboardBumdesRoute,
|
|
||||||
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
|
|
||||||
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,
|
|
||||||
DashboardKeamananRoute: DashboardKeamananRoute,
|
|
||||||
DashboardKeuanganAnggaranRoute: DashboardKeuanganAnggaranRoute,
|
|
||||||
DashboardKinerjaDivisiRoute: DashboardKinerjaDivisiRoute,
|
|
||||||
DashboardPengaduanLayananPublikRoute: DashboardPengaduanLayananPublikRoute,
|
|
||||||
DashboardSosialRoute: DashboardSosialRoute,
|
|
||||||
DashboardIndexRoute: DashboardIndexRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
|
|
||||||
DashboardRouteRouteChildren,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AdminRouteRoute: AdminRouteRouteWithChildren,
|
AdminRouteRoute: AdminRouteRouteWithChildren,
|
||||||
DashboardRouteRoute: DashboardRouteRouteWithChildren,
|
PengaturanRouteRoute: PengaturanRouteRouteWithChildren,
|
||||||
|
BantuanRoute: BantuanRoute,
|
||||||
|
BumdesRoute: BumdesRoute,
|
||||||
|
DemografiPekerjaanRoute: DemografiPekerjaanRoute,
|
||||||
|
JennaAnalyticRoute: JennaAnalyticRoute,
|
||||||
|
KeamananRoute: KeamananRoute,
|
||||||
|
KeuanganAnggaranRoute: KeuanganAnggaranRoute,
|
||||||
|
KinerjaDivisiRoute: KinerjaDivisiRoute,
|
||||||
|
PengaduanLayananPublikRoute: PengaduanLayananPublikRoute,
|
||||||
SigninRoute: SigninRoute,
|
SigninRoute: SigninRoute,
|
||||||
SignupRoute: SignupRoute,
|
SignupRoute: SignupRoute,
|
||||||
|
SosialRoute: SosialRoute,
|
||||||
ProfileEditRoute: ProfileEditRoute,
|
ProfileEditRoute: ProfileEditRoute,
|
||||||
UsersIdRoute: UsersIdRoute,
|
UsersIdRoute: UsersIdRoute,
|
||||||
ProfileIndexRoute: ProfileIndexRoute,
|
ProfileIndexRoute: ProfileIndexRoute,
|
||||||
|
|||||||
@@ -7,8 +7,20 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
|
|||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
beforeLoad: protectedRouteMiddleware,
|
beforeLoad: async ({ location }) => {
|
||||||
onEnter({ context }) {
|
// Only apply auth middleware for routes that need it
|
||||||
|
// Public routes: /, /signin, /signup
|
||||||
|
const isPublicRoute =
|
||||||
|
location.pathname === "/" ||
|
||||||
|
location.pathname === "/signin" ||
|
||||||
|
location.pathname === "/signup";
|
||||||
|
|
||||||
|
if (isPublicRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply protected route middleware for all other routes
|
||||||
|
const context = await protectedRouteMiddleware({ location });
|
||||||
authStore.user = context?.user as any;
|
authStore.user = context?.user as any;
|
||||||
authStore.session = context?.session as any;
|
authStore.session = context?.session as any;
|
||||||
},
|
},
|
||||||
|
|||||||
66
src/routes/bantuan.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import HelpPage from "@/components/help-page";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/bantuan")({
|
||||||
|
component: BantuanRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BantuanRoute() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<HelpPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/routes/bumdes.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import BumdesPage from "@/components/bumdes-page";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/bumdes")({
|
||||||
|
component: BumdesRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BumdesRoute() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<BumdesPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import HelpPage from "@/components/help-page";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/bantuan")({
|
|
||||||
component: HelpPage,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import BumdesPage from "@/components/bumdes-page";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/bumdes")({
|
|
||||||
component: BumdesPage,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import DemografiPekerjaan from "../../components/demografi-pekerjaan";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/demografi-pekerjaan")({
|
|
||||||
component: DemografiPekerjaan,
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { DashboardContent } from "@/components/dashboard-content";
|
|
||||||
export const Route = createFileRoute("/dashboard/")({
|
|
||||||
component: DashboardContent,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import JennaAnalytic from "@/components/jenna-analytic";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/jenna-analytic")({
|
|
||||||
component: JennaAnalytic,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import KeamananPage from "@/components/keamanan-page";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/keamanan")({
|
|
||||||
component: KeamananPage,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/keuangan-anggaran")({
|
|
||||||
component: KeuanganAnggaran,
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import KinerjaDivisi from "@/components/kinerja-divisi";
|
|
||||||
export const Route = createFileRoute("/dashboard/kinerja-divisi")({
|
|
||||||
component: KinerjaDivisi,
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaduan-layanan-publik")({
|
|
||||||
component: PengaduanLayananPublik,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import AksesDanTimSettings from "@/components/pengaturan/akses-dan-tim";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaturan/akses-dan-tim")({
|
|
||||||
component: AksesDanTimSettings,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import KeamananSettings from "@/components/pengaturan/keamanan";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaturan/keamanan")({
|
|
||||||
component: KeamananSettings,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import NotifikasiSettings from "@/components/pengaturan/notifikasi";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaturan/notifikasi")({
|
|
||||||
component: NotifikasiSettings,
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaturan")({
|
|
||||||
component: () => (
|
|
||||||
<div className="p-2">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import SocialPage from "@/components/sosial-page";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/sosial")({
|
|
||||||
component: SocialPage,
|
|
||||||
});
|
|
||||||
66
src/routes/demografi-pekerjaan.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
import DemografiPekerjaan from "../components/demografi-pekerjaan";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/demografi-pekerjaan")({
|
||||||
|
component: DemografiPekerjaanPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function DemografiPekerjaanPage() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<DemografiPekerjaan />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,788 +1,62 @@
|
|||||||
import {
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
ActionIcon,
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
Avatar,
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
Box,
|
import { DashboardContent } from "@/components/dashboard-content";
|
||||||
Button,
|
import { Header } from "@/components/header";
|
||||||
Card,
|
import { Sidebar } from "@/components/sidebar";
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
Paper,
|
|
||||||
rem,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
Transition,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconApi,
|
|
||||||
IconBolt,
|
|
||||||
IconBrandGithub,
|
|
||||||
IconBrandLinkedin,
|
|
||||||
IconBrandTwitter,
|
|
||||||
IconChevronRight,
|
|
||||||
IconLock,
|
|
||||||
IconMoon,
|
|
||||||
IconRocket,
|
|
||||||
IconShield,
|
|
||||||
IconStack2,
|
|
||||||
IconSun,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: HomePage,
|
component: DashboardPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigation items
|
function DashboardPage() {
|
||||||
const NAV_ITEMS = [
|
const [opened, { toggle }] = useDisclosure();
|
||||||
{ label: "Home", link: "/" },
|
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
|
||||||
{ label: "Features", link: "#features" },
|
const { colorScheme } = useMantineColorScheme();
|
||||||
{ label: "Testimonials", link: "#testimonials" },
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
{ label: "Pricing", link: "/pricing" },
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
{ label: "Contact", link: "/contact" },
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
];
|
|
||||||
|
|
||||||
// Features data
|
const handleMainClick = () => {
|
||||||
const FEATURES = [
|
if (!sidebarCollapsed) {
|
||||||
{
|
setSidebarCollapsed.toggle();
|
||||||
icon: IconBolt,
|
}
|
||||||
title: "Lightning Fast",
|
};
|
||||||
description: "Built on Bun runtime for exceptional performance and speed.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconShield,
|
|
||||||
title: "Secure by Design",
|
|
||||||
description:
|
|
||||||
"Enterprise-grade authentication with Better Auth integration.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconApi,
|
|
||||||
title: "RESTful API",
|
|
||||||
description:
|
|
||||||
"Full-featured API with Elysia.js for seamless backend operations.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconStack2,
|
|
||||||
title: "Modern Stack",
|
|
||||||
description: "React 19, TanStack Router, and Mantine UI for the best DX.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconLock,
|
|
||||||
title: "API Key Auth",
|
|
||||||
description: "Secure API key management for external integrations.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconRocket,
|
|
||||||
title: "Production Ready",
|
|
||||||
description: "Type-safe, tested, and optimized for production deployment.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Testimonials data
|
|
||||||
const TESTIMONIALS = [
|
|
||||||
{
|
|
||||||
id: "testimonial-1",
|
|
||||||
name: "Alex Johnson",
|
|
||||||
role: "Lead Developer",
|
|
||||||
content:
|
|
||||||
"This template saved us weeks of setup time. The architecture is clean and well-thought-out.",
|
|
||||||
avatar:
|
|
||||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "testimonial-2",
|
|
||||||
name: "Sarah Williams",
|
|
||||||
role: "CTO",
|
|
||||||
content:
|
|
||||||
"The performance improvements we saw after switching to this stack were remarkable. Highly recommended!",
|
|
||||||
avatar:
|
|
||||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "testimonial-3",
|
|
||||||
name: "Michael Chen",
|
|
||||||
role: "Product Manager",
|
|
||||||
content:
|
|
||||||
"The developer experience is top-notch. Everything is well-documented and easy to extend.",
|
|
||||||
avatar:
|
|
||||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function NavigationBar() {
|
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
|
||||||
const [scrolled, setScrolled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setScrolled(window.scrollY > 20);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll);
|
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<AppShell
|
||||||
h={70}
|
header={{ height: 60 }}
|
||||||
px="md"
|
navbar={{
|
||||||
style={{
|
width: 300,
|
||||||
borderBottom: "1px solid var(--mantine-color-gray-2)",
|
breakpoint: "sm",
|
||||||
transition: "all 0.3s ease",
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
boxShadow: scrolled ? "0 2px 10px rgba(0,0,0,0.1)" : "none",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
}}
|
||||||
|
padding="md"
|
||||||
>
|
>
|
||||||
<Group h="100%" justify="space-between">
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group>
|
<Group h="100%" px="md">
|
||||||
<Link to="/" style={{ textDecoration: "none" }}>
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||||
<Title order={3} c="blue">
|
<Header onSidebarToggle={setSidebarCollapsed.toggle} />
|
||||||
BunStack
|
|
||||||
</Title>
|
|
||||||
</Link>
|
|
||||||
<Group ml={50} visibleFrom="sm" gap="lg">
|
|
||||||
{NAV_ITEMS.map((item) => {
|
|
||||||
const isActive = window.location.pathname === item.link;
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={item.label}
|
|
||||||
component={Link}
|
|
||||||
to={item.link}
|
|
||||||
style={{
|
|
||||||
textDecoration: "none",
|
|
||||||
fontSize: rem(16),
|
|
||||||
padding: `${rem(8)} ${rem(12)}`,
|
|
||||||
borderRadius: rem(6),
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
color: isActive
|
|
||||||
? "var(--mantine-color-blue-6)"
|
|
||||||
: "var(--mantine-color-dimmed)",
|
|
||||||
fontWeight: 500,
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "block",
|
|
||||||
}}
|
|
||||||
className="nav-item"
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
<Group>
|
<AppShell.Navbar
|
||||||
<ActionIcon
|
p="md"
|
||||||
variant="default"
|
bg={navbarBgColor}
|
||||||
onClick={() => toggleColorScheme()}
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{colorScheme === "dark" ? (
|
|
||||||
<IconSun size={18} />
|
|
||||||
) : (
|
|
||||||
<IconMoon size={18} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
|
||||||
<Button component={Link} to="/signin" variant="light" size="sm">
|
|
||||||
Sign In
|
|
||||||
</Button>
|
|
||||||
<Button component={Link} to="/signup" size="sm">
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HeroSection() {
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoaded(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Simulate delay for image transition
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setImageLoaded(true);
|
|
||||||
}, 200);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
pt={rem(140)} // Adjusted padding for simpler header
|
|
||||||
pb={rem(60)}
|
|
||||||
>
|
|
||||||
<Container size="lg">
|
|
||||||
<Grid gutter={{ base: rem(40), md: rem(80) }} align="center">
|
|
||||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
|
||||||
<Transition
|
|
||||||
mounted={loaded}
|
|
||||||
transition="slide-up"
|
|
||||||
duration={600}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<Stack gap="xl" style={styles}>
|
|
||||||
<Title
|
|
||||||
order={1}
|
|
||||||
style={{
|
|
||||||
fontSize: rem(48),
|
|
||||||
fontWeight: 900,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Build Faster with{" "}
|
|
||||||
<Text span c="blue" inherit>
|
|
||||||
Bun Stack
|
|
||||||
</Text>
|
|
||||||
</Title>
|
|
||||||
<Text size="xl" c="dimmed">
|
|
||||||
A modern, full-stack React template powered by Bun,
|
|
||||||
Elysia.js, and TanStack Router. Ship your ideas faster than
|
|
||||||
ever.
|
|
||||||
</Text>
|
|
||||||
<Group gap="md">
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/admin"
|
|
||||||
size="lg"
|
|
||||||
variant="filled"
|
|
||||||
rightSection={<IconRocket size="1.25rem" />}
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/docs"
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
|
||||||
<Transition
|
|
||||||
mounted={imageLoaded}
|
|
||||||
transition="slide-left"
|
|
||||||
duration={800}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<Paper shadow="xl" radius="lg" p="md" withBorder style={styles}>
|
|
||||||
<Image
|
|
||||||
src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
|
|
||||||
alt="Code editor showing Bun Stack code"
|
|
||||||
radius="md"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AnimatedFeatureCard({
|
|
||||||
feature,
|
|
||||||
index,
|
|
||||||
isVisible,
|
|
||||||
}: {
|
|
||||||
feature: (typeof FEATURES)[number];
|
|
||||||
index: number;
|
|
||||||
isVisible: boolean;
|
|
||||||
}) {
|
|
||||||
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isVisible) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsDelayedVisible(true);
|
|
||||||
}, index * 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isVisible, index]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mounted={isDelayedVisible}
|
|
||||||
transition="slide-up"
|
|
||||||
duration={500}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<Card
|
|
||||||
className="feature-card"
|
|
||||||
padding="lg"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
shadow="sm"
|
|
||||||
style={styles}
|
|
||||||
>
|
|
||||||
<ThemeIcon variant="light" color="blue" size={60} radius="md">
|
|
||||||
<feature.icon size="1.75rem" />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Stack gap={8} mt="md">
|
|
||||||
<Title order={4}>{feature.title}</Title>
|
|
||||||
<Text size="sm" c="dimmed" lh={1.5}>
|
|
||||||
{feature.description}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FeaturesSection() {
|
|
||||||
const [visibleFeatures, setVisibleFeatures] = useState(
|
|
||||||
Array(FEATURES.length).fill(false),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry, index) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisibleFeatures((prev) => {
|
|
||||||
const newVisible = [...prev];
|
|
||||||
newVisible[index] = true;
|
|
||||||
return newVisible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const elements = document.querySelectorAll(".feature-card");
|
|
||||||
elements.forEach((el) => {
|
|
||||||
observer.observe(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container size="lg" py={rem(80)}>
|
|
||||||
<Stack gap="xl" align="center" mb={rem(50)}>
|
|
||||||
<Transition
|
|
||||||
mounted={true}
|
|
||||||
transition="fade"
|
|
||||||
duration={600}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<div style={styles}>
|
|
||||||
<Title order={2} ta="center">
|
|
||||||
Everything You Need
|
|
||||||
</Title>
|
|
||||||
<Text c="dimmed" size="lg" ta="center" maw={600}>
|
|
||||||
A complete toolkit for building modern web applications with
|
|
||||||
best practices built-in.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Stack>
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
|
||||||
{FEATURES.map((feature, index) => (
|
|
||||||
<AnimatedFeatureCard
|
|
||||||
key={feature.title}
|
|
||||||
feature={feature}
|
|
||||||
index={index}
|
|
||||||
isVisible={visibleFeatures[index]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AnimatedTestimonialCard({
|
|
||||||
testimonial,
|
|
||||||
index,
|
|
||||||
isVisible,
|
|
||||||
}: {
|
|
||||||
testimonial: (typeof TESTIMONIALS)[number];
|
|
||||||
index: number;
|
|
||||||
isVisible: boolean;
|
|
||||||
}) {
|
|
||||||
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isVisible) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsDelayedVisible(true);
|
|
||||||
}, index * 150);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isVisible, index]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mounted={isDelayedVisible}
|
|
||||||
transition="slide-up"
|
|
||||||
duration={500}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<Card
|
|
||||||
padding="lg"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
shadow="sm"
|
|
||||||
className="testimonial-card"
|
|
||||||
style={styles}
|
|
||||||
>
|
|
||||||
<Text c="dimmed" mb="md">
|
|
||||||
"{testimonial.content}"
|
|
||||||
</Text>
|
|
||||||
<Group>
|
|
||||||
<Avatar src={testimonial.avatar} size="md" radius="xl" />
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text fw={600}>{testimonial.name}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{testimonial.role}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestimonialsSection() {
|
|
||||||
const [visibleTestimonials, setVisibleTestimonials] = useState(
|
|
||||||
Array(TESTIMONIALS.length).fill(false),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry, index) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisibleTestimonials((prev) => {
|
|
||||||
const newVisible = [...prev];
|
|
||||||
newVisible[index] = true;
|
|
||||||
return newVisible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const elements = document.querySelectorAll(".testimonial-card");
|
|
||||||
elements.forEach((el) => {
|
|
||||||
observer.observe(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box py={rem(80)}>
|
|
||||||
<Container size="lg">
|
|
||||||
<Stack gap="xl" align="center" mb={rem(50)}>
|
|
||||||
<Transition
|
|
||||||
mounted={true}
|
|
||||||
transition="fade"
|
|
||||||
duration={600}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<div style={styles}>
|
|
||||||
<Title order={2} ta="center">
|
|
||||||
Loved by Developers
|
|
||||||
</Title>
|
|
||||||
<Text c="dimmed" size="lg" ta="center" maw={600}>
|
|
||||||
Join thousands of satisfied developers who have accelerated
|
|
||||||
their projects with Bun Stack.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
|
||||||
{TESTIMONIALS.map((testimonial, index) => (
|
|
||||||
<AnimatedTestimonialCard
|
|
||||||
key={testimonial.id}
|
|
||||||
testimonial={testimonial}
|
|
||||||
index={index}
|
|
||||||
isVisible={visibleTestimonials[index]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CtaSection() {
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoaded(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container size="lg" py={rem(80)}>
|
|
||||||
<Transition
|
|
||||||
mounted={loaded}
|
|
||||||
transition="slide-up"
|
|
||||||
duration={600}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
>
|
||||||
{(styles) => (
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
<Paper
|
<Sidebar />
|
||||||
radius="lg"
|
</div>
|
||||||
p={rem(60)}
|
</AppShell.Navbar>
|
||||||
bg="blue"
|
|
||||||
style={{
|
<AppShell.Main
|
||||||
...styles,
|
bg={mainBgColor}
|
||||||
background:
|
onClick={handleMainClick}
|
||||||
"linear-gradient(135deg, var(--mantine-color-blue-6), var(--mantine-color-indigo-6))",
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
}}
|
>
|
||||||
>
|
<DashboardContent />
|
||||||
<Stack align="center" gap="xl" ta="center">
|
</AppShell.Main>
|
||||||
<Title c="white" order={2}>
|
</AppShell>
|
||||||
Ready to get started?
|
|
||||||
</Title>
|
|
||||||
<Text c="white" size="lg" maw={600}>
|
|
||||||
Join thousands of developers who are building faster and more
|
|
||||||
reliable applications with Bun Stack.
|
|
||||||
</Text>
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/signup"
|
|
||||||
size="lg"
|
|
||||||
variant="white"
|
|
||||||
color="dark"
|
|
||||||
rightSection={<IconChevronRight size="1.125rem" />}
|
|
||||||
>
|
|
||||||
Create Account
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/docs"
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
color="white"
|
|
||||||
>
|
|
||||||
View Documentation
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Footer() {
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setLoaded(true);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mounted={loaded}
|
|
||||||
transition="slide-up"
|
|
||||||
duration={600}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<Box
|
|
||||||
py={rem(40)}
|
|
||||||
style={{
|
|
||||||
...styles,
|
|
||||||
borderTop: "1px solid var(--mantine-color-gray-2)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container size="lg">
|
|
||||||
<Grid gutter={{ base: rem(40), md: rem(80) }}>
|
|
||||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Title order={3}>BunStack</Title>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
The ultimate full-stack solution for modern web
|
|
||||||
applications.
|
|
||||||
</Text>
|
|
||||||
<Group>
|
|
||||||
<ActionIcon size="lg" variant="subtle" color="gray">
|
|
||||||
<IconBrandGithub size="1.25rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon size="lg" variant="subtle" color="gray">
|
|
||||||
<IconBrandTwitter size="1.25rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon size="lg" variant="subtle" color="gray">
|
|
||||||
<IconBrandLinkedin size="1.25rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={4}>Product</Title>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/features"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
Features
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/pricing"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
Pricing
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/docs"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={4}>Company</Title>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/about"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/blog"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
Blog
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component={Link}
|
|
||||||
to="/careers"
|
|
||||||
td="none"
|
|
||||||
>
|
|
||||||
Careers
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={4}>Subscribe to our newsletter</Title>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Get the latest news and updates
|
|
||||||
</Text>
|
|
||||||
<Group>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="Your email"
|
|
||||||
style={{
|
|
||||||
padding: "8px 12px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid var(--mantine-color-gray-3)",
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button>Subscribe</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
pt={rem(40)}
|
|
||||||
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
© 2024 Bun Stack. Built with Bun, Elysia, and React.
|
|
||||||
</Text>
|
|
||||||
<Group gap="lg">
|
|
||||||
<Text
|
|
||||||
component={Link}
|
|
||||||
to="/privacy"
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
style={{ textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
component={Link}
|
|
||||||
to="/terms"
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
style={{ textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomePage() {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<NavigationBar />
|
|
||||||
<HeroSection />
|
|
||||||
<FeaturesSection />
|
|
||||||
<TestimonialsSection />
|
|
||||||
<CtaSection />
|
|
||||||
<Footer />
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/routes/jenna-analytic.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import JennaAnalytic from "@/components/jenna-analytic";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/jenna-analytic")({
|
||||||
|
component: JennaAnalyticPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function JennaAnalyticPage() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<JennaAnalytic />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/routes/keamanan.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import KeamananPage from "@/components/keamanan-page";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/keamanan")({
|
||||||
|
component: KeamananRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function KeamananRoute() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<KeamananPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/routes/keuangan-anggaran.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/keuangan-anggaran")({
|
||||||
|
component: KeuanganAnggaranPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function KeuanganAnggaranPage() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<KeuanganAnggaran />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/routes/kinerja-divisi.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import KinerjaDivisi from "@/components/kinerja-divisi";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/kinerja-divisi")({
|
||||||
|
component: KinerjaDivisiPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function KinerjaDivisiPage() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<KinerjaDivisi />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/routes/pengaduan-layanan-publik.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/pengaduan-layanan-publik")({
|
||||||
|
component: PengaduanLayananPublikPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PengaduanLayananPublikPage() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<PengaduanLayananPublik />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/routes/pengaturan/akses-dan-tim.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/pengaturan/akses-dan-tim"!</div>;
|
||||||
|
}
|
||||||
9
src/routes/pengaturan/keamanan.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/pengaturan/keamanan")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/pengaturan/keamanan"!</div>;
|
||||||
|
}
|
||||||
9
src/routes/pengaturan/notifikasi.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/pengaturan/notifikasi")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/pengaturan/notifikasi"!</div>;
|
||||||
|
}
|
||||||
@@ -1,38 +1,42 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
AppShell,
|
createFileRoute,
|
||||||
Burger,
|
Outlet,
|
||||||
Group,
|
useRouterState,
|
||||||
useMantineColorScheme,
|
} from "@tanstack/react-router";
|
||||||
useMantineTheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
|
||||||
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard")({
|
export const Route = createFileRoute("/pengaturan")({
|
||||||
component: RouteComponent,
|
component: PengaturanLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function PengaturanLayout() {
|
||||||
const [opened, { toggle, close }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const theme = useMantineTheme();
|
|
||||||
const routerState = useRouterState();
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||||
|
const routerState = useRouterState();
|
||||||
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
// ✅ AUTO CLOSE NAVBAR ON ROUTE CHANGE (MOBILE ONLY)
|
// Auto close navbar on route change (mobile only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile && opened) {
|
if (isMobile && opened) {
|
||||||
close();
|
toggleMobile();
|
||||||
}
|
}
|
||||||
}, [routerState.location.pathname]);
|
}, [routerState.location.pathname, isMobile, opened, toggleMobile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
@@ -40,25 +44,19 @@ function RouteComponent() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group
|
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
||||||
h="100%"
|
|
||||||
px="lg"
|
|
||||||
align="center"
|
|
||||||
wrap="nowrap"
|
|
||||||
>
|
|
||||||
<Burger
|
<Burger
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClick={toggle}
|
onClick={toggleMobile}
|
||||||
hiddenFrom="sm"
|
hiddenFrom="sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
<Header />
|
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -72,8 +70,14 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
<Outlet />
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<div className="p-2">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import UmumSettings from "@/components/pengaturan/umum";
|
import UmumSettings from "@/components/pengaturan/umum";
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/pengaturan/umum")({
|
export const Route = createFileRoute("/pengaturan/umum")({
|
||||||
component: UmumSettings,
|
component: UmumSettings,
|
||||||
});
|
});
|
||||||
66
src/routes/sosial.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import SosialPage from "@/components/sosial-page";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/sosial")({
|
||||||
|
component: SosialRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function SosialRoute() {
|
||||||
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<SosialPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/store/sosial.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
type SelectedYear = string;
|
||||||
|
|
||||||
|
interface SosialState {
|
||||||
|
selectedYear: SelectedYear;
|
||||||
|
filters: {
|
||||||
|
dusun: string | null;
|
||||||
|
kategori: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sosialStore = proxy<SosialState>({
|
||||||
|
selectedYear: new Date().getFullYear().toString(),
|
||||||
|
filters: {
|
||||||
|
dusun: null,
|
||||||
|
kategori: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setYear = (year: SelectedYear) => {
|
||||||
|
sosialStore.selectedYear = year;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setFilter = (
|
||||||
|
key: keyof SosialState["filters"],
|
||||||
|
value: string | null,
|
||||||
|
) => {
|
||||||
|
sosialStore.filters[key] = value;
|
||||||
|
};
|
||||||
30
src/store/umkm.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
type TimeRange = "minggu" | "bulan";
|
||||||
|
|
||||||
|
interface UmkmState {
|
||||||
|
selectedRange: TimeRange;
|
||||||
|
filters: {
|
||||||
|
kategori: string | null;
|
||||||
|
umkm: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const umkmStore = proxy<UmkmState>({
|
||||||
|
selectedRange: "bulan",
|
||||||
|
filters: {
|
||||||
|
kategori: null,
|
||||||
|
umkm: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setRange = (range: TimeRange) => {
|
||||||
|
umkmStore.selectedRange = range;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setFilter = (
|
||||||
|
key: keyof UmkmState["filters"],
|
||||||
|
value: string | null,
|
||||||
|
) => {
|
||||||
|
umkmStore.filters[key] = value;
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ export const auth = betterAuth({
|
|||||||
clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING",
|
clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING",
|
||||||
clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING",
|
clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
redirectURI: `${baseUrl}/api/auth/callback/github`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VITE_PUBLIC_URL = (() => {
|
export const VITE_PUBLIC_URL = (() => {
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1. BETTER_AUTH_URL (standard for better-auth)
|
// 1. BETTER_AUTH_URL (standard for better-auth)
|
||||||
// 2. VITE_PUBLIC_URL (our app standard)
|
// 2. VITE_PUBLIC_URL (our app standard)
|
||||||
// 3. window.location.origin (browser fallback)
|
// 3. window.location.origin (browser fallback)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite";
|
|||||||
export async function createVite() {
|
export async function createVite() {
|
||||||
return createViteServer({
|
return createViteServer({
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
|
publicDir: "public",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(process.cwd(), "./src"),
|
"@": path.resolve(process.cwd(), "./src"),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: [
|
||||||
|
"./src/index.html",
|
||||||
|
"./public/**/*.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
"darmasaba-navy": {
|
"darmasaba-navy": {
|
||||||
DEFAULT: "#1E3A5F", // Primary navy color
|
DEFAULT: "#1E3A5F",
|
||||||
50: "#E1E4F2",
|
50: "#E1E4F2",
|
||||||
100: "#B9C2DD",
|
100: "#B9C2DD",
|
||||||
200: "#91A0C9",
|
200: "#91A0C9",
|
||||||
@@ -18,7 +22,7 @@ module.exports = {
|
|||||||
900: "#071833",
|
900: "#071833",
|
||||||
},
|
},
|
||||||
"darmasaba-blue": {
|
"darmasaba-blue": {
|
||||||
DEFAULT: "#3B82F6", // Primary blue color
|
DEFAULT: "#3B82F6",
|
||||||
50: "#E3F0FF",
|
50: "#E3F0FF",
|
||||||
100: "#B6D9FF",
|
100: "#B6D9FF",
|
||||||
200: "#89C2FF",
|
200: "#89C2FF",
|
||||||
|
|||||||