Compare commits

..

8 Commits

6 changed files with 117 additions and 24 deletions

81
CLAUDE.md Normal file
View File

@@ -0,0 +1,81 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Development
bun install # Install dependencies
bun run dev # Dev server with experimental HTTPS (localhost:3000)
bun run build # Production build
bun run start # Start production server
bun run lint # Run ESLint
# Database
npx prisma migrate dev # Run/create migrations
npx prisma db seed # Seed with initial data
npx prisma generate # Regenerate Prisma client after schema changes
```
## Architecture
**Sistem Desa Mandiri** is a village administration platform built on Next.js 14 (App Router) with PostgreSQL.
### Key Layers
- **`src/app/(application)/`** — Auth-protected pages grouped by feature (announcement, division, project, discussion, member, profile, home, group)
- **`src/app/(auth)/`** — Login/register pages
- **`src/app/api/`** — REST API endpoints; subdirectories map to resource types (`/api/announcement`, `/api/project`, `/api/task`, etc.). Mobile-specific endpoints live under `/api/mobile/`
- **`src/module/`** — Business logic modules, one per feature (19 modules). Each module contains hooks, components, and API call functions for that domain
- **`src/lib/`** — Shared utilities: Prisma client singleton (`prisma.ts`), Firebase init, route definitions (`routes.ts`), push notification hooks
### Data Access Pattern
All DB access goes through the Prisma client singleton in `src/lib/prisma.ts`. Prisma schema is at `prisma/schema.prisma` (40+ models). Migrations live in `prisma/migrations/`.
### State Management
- **Hookstate** (`@hookstate/core` + `@hookstate/localstored`) for client-side global state with localStorage persistence
- **Iron-session** for server-side session management / auth
- **Jose** for JWT handling
### UI Stack
- **Mantine 7** is the primary UI library (components, forms, modals, notifications, charts, dates, etc.)
- **Tailwind CSS** for utility classes — used alongside Mantine
- **PostCSS** configured with Mantine preset (`postcss.config.mjs`)
### Real-time & Notifications
- **Firebase FCM** (`src/lib/firebase/`) for mobile push notifications
- **Web Push + VAPID keys** (`src/lib/usePushNotifications.ts`) for browser push
- **wibu-realtime** (custom library) for WebSocket-based real-time updates
### User Roles
Five roles with distinct access levels (see `PANDUAN PENGGUNAAN.md`):
1. **Super Admin** — full system access
2. **Admin Desa** — village-level administration
3. **Ketua Divisi** — division leader
4. **Anggota Divisi** — division member
5. **Warga/Perangkat Desa** — village resident/official
## Environment Variables
Copy `.env.example` to `.env`. Required variables:
| Variable | Purpose |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string |
| `GOOGLE_PROJECT_ID`, `GOOGLE_CLIENT_EMAIL`, `GOOGLE_PRIVATE_KEY` | Firebase Admin SDK (FCM) |
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` | Web Push |
| `WS_APIKEY` | WebSocket/file storage API key |
| `WIBU_REALTIME_KEY` | Real-time communication |
| `FCM_KEY` | Firebase Cloud Messaging |
## Deployment
Docker images are built via `.github/workflows/publish.yml` and pushed to GHCR (`ghcr.io`). Portainer redeploys via `.github/workflows/re-pull.yml`. Supports `dev`, `stg`, and `prod` stacks.
The Dockerfile uses a two-stage build: Bun builder → Bun runner (non-root user, port 3000).

View File

@@ -7,7 +7,7 @@ export async function POST(req: NextRequest) {
const { phone }: ILogin = await req.json();
const user = await prisma.user.findUnique({
where: { phone, isActive: true },
select: { id: true, phone: true, isWithoutOTP: true },
select: { id: true, phone: true, isWithoutOTP: true, Village: { select: { isActive: true } } },
});
if (!user) {
@@ -17,6 +17,13 @@ export async function POST(req: NextRequest) {
});
}
if (!user.Village?.isActive) {
return Response.json({
success: false,
message: "Akun anda tidak aktif, silahkan hubungi admin",
});
}
return Response.json({
success: true,
message: "Sukses",

View File

@@ -2,6 +2,7 @@ import { prisma } from "@/module/_global";
import { funGetUserById } from "@/module/auth";
import _, { ceil } from "lodash";
import { NextResponse } from "next/server";
import moment from "moment";
export async function GET(request: Request) {
try {
@@ -38,10 +39,10 @@ export async function GET(request: Request) {
DivisionProjectTask: {
some: {
dateStart: {
gte: new Date(String(date))
gte: moment(String(date)).startOf('day').toDate()
},
dateEnd: {
lte: new Date(String(dateAkhir))
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}
@@ -54,10 +55,10 @@ export async function GET(request: Request) {
DivisionProjectTask: {
some: {
dateStart: {
gte: new Date(String(date))
gte: moment(String(date)).startOf('day').toDate()
},
dateEnd: {
lte: new Date(String(dateAkhir))
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}
@@ -102,10 +103,10 @@ export async function GET(request: Request) {
DivisionProjectTask: {
some: {
dateStart: {
gte: new Date(String(date))
gte: moment(String(date)).startOf('day').toDate()
},
dateEnd: {
lte: new Date(String(dateAkhir))
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}
@@ -117,10 +118,10 @@ export async function GET(request: Request) {
DivisionProjectTask: {
some: {
dateStart: {
gte: new Date(String(date))
gte: moment(String(date)).startOf('day').toDate()
},
dateEnd: {
lte: new Date(String(dateAkhir))
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}
@@ -171,8 +172,8 @@ export async function GET(request: Request) {
idGroup: String(grup)
},
createdAt: {
gte: new Date(String(date)),
lte: new Date(String(dateAkhir))
gte: moment(String(date)).startOf('day').toDate(),
lte: moment(String(dateAkhir)).endOf('day').toDate()
},
}
} else {
@@ -181,8 +182,8 @@ export async function GET(request: Request) {
category: 'FILE',
idDivision: String(division),
createdAt: {
gte: new Date(String(date)),
lte: new Date(String(dateAkhir))
gte: moment(String(date)).startOf('day').toDate(),
lte: moment(String(dateAkhir)).endOf('day').toDate()
},
}
}
@@ -252,8 +253,8 @@ export async function GET(request: Request) {
DivisionCalendarReminder: {
some: {
dateStart: {
gte: new Date(String(date)),
lte: new Date()
gte: moment(String(date)).startOf('day').toDate(),
lte: moment().toDate()
}
}
}
@@ -267,8 +268,8 @@ export async function GET(request: Request) {
DivisionCalendarReminder: {
some: {
dateStart: {
gt: new Date(),
lte: new Date(String(dateAkhir))
gt: moment().toDate(),
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}
@@ -293,8 +294,8 @@ export async function GET(request: Request) {
DivisionCalendarReminder: {
some: {
dateStart: {
gte: new Date(String(date)),
lte: new Date()
gte: moment(String(date)).startOf('day').toDate(),
lte: moment().toDate()
}
}
}
@@ -306,8 +307,8 @@ export async function GET(request: Request) {
DivisionCalendarReminder: {
some: {
dateStart: {
gt: new Date(),
lte: new Date(String(dateAkhir))
gt: moment().toDate(),
lte: moment(String(dateAkhir)).endOf('day').toDate()
}
}
}

View File

@@ -44,7 +44,8 @@ export async function GET(request: Request, context: { params: { id: string } })
},
Village:{
select:{
name:true
name:true,
isActive:true,
}
}
},
@@ -57,8 +58,9 @@ export async function GET(request: Request, context: { params: { id: string } })
const phone = users?.phone.substr(2)
const role = users?.UserRole.name
const village = users?.Village.name
const villageIsActive = users?.Village.isActive
const result = { ...userData, group, position, idUserRole, phone, role, village };
const result = { ...userData, group, position, idUserRole, phone, role, village, villageIsActive };
const omitData = _.omit(result, ["Group", "Position", "UserRole", "Village"]);

View File

@@ -490,6 +490,7 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" })
name: true,
isActive: true,
createdAt: true,
updatedAt: true,
desc: true,
User: {
where: {
@@ -518,6 +519,7 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" })
isActive: data?.isActive,
desc: data?.desc,
createdAt: data?.createdAt ? formatDateTime(data.createdAt) : null,
updatedAt: data?.updatedAt ? formatDateTime(data.updatedAt) : null,
perbekel: data?.User[0]?.name || null,
} : null;

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
return NextResponse.json({ success: true, version: "2.1.9", tahap: "beta", update: "-api untuk dashboard monitoring" }, { status: 200 });
return NextResponse.json({ success: true, version: "2.1.10", tahap: "beta", update: "-perbaikan grafik divisi" }, { status: 200 });
} catch (error) {
console.error(error);
return NextResponse.json({ success: false, version: "Gagal mendapatkan version, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });