feat(keamanan): tambah modul CCTV — schema, API, admin UI, seeder
- Tambah model CctvKeamanan + enum StatusCctv ke prisma schema - Tambah status Baru ke enum StatusLaporan - Migration: add_cctv_keamanan_model - API CRUD + stats endpoint di /api/keamanan/cctv/... - Admin state (valtio proxy) dengan create/findMany/edit/delete/stats - Admin pages: list, create, detail (peta Leaflet), edit (peta picker) - Seeder 8 data CCTV lokasi Darmasaba - Tambah submenu CCTV di sidebar nav keamanan - Bump version 0.1.57 → 0.1.58 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
src/app/api/[[...slugs]]/_lib/keamanan/cctv/create.ts
Normal file
33
src/app/api/[[...slugs]]/_lib/keamanan/cctv/create.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type CctvInput = {
|
||||
kode: string;
|
||||
nama: string;
|
||||
lokasi: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
status?: "Online" | "Offline";
|
||||
lastActive?: string;
|
||||
};
|
||||
|
||||
const cctvCreate = async (context: Context) => {
|
||||
const { kode, nama, lokasi, latitude, longitude, status, lastActive } =
|
||||
(await context.body) as CctvInput;
|
||||
|
||||
const data = await prisma.cctvKeamanan.create({
|
||||
data: {
|
||||
kode,
|
||||
nama,
|
||||
lokasi,
|
||||
latitude,
|
||||
longitude,
|
||||
status: status ?? "Online",
|
||||
lastActive: lastActive ? new Date(lastActive) : new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, data };
|
||||
};
|
||||
|
||||
export default cctvCreate;
|
||||
26
src/app/api/[[...slugs]]/_lib/keamanan/cctv/del.ts
Normal file
26
src/app/api/[[...slugs]]/_lib/keamanan/cctv/del.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
const cctvDelete = async (context: Context) => {
|
||||
const id = context.params.id as string;
|
||||
|
||||
try {
|
||||
const cctv = await prisma.cctvKeamanan.findUnique({ where: { id } });
|
||||
|
||||
if (!cctv) {
|
||||
return { success: false, message: "CCTV tidak ditemukan" };
|
||||
}
|
||||
|
||||
await prisma.cctvKeamanan.update({
|
||||
where: { id },
|
||||
data: { isActive: false, deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return { success: true, message: "CCTV berhasil dihapus" };
|
||||
} catch (error) {
|
||||
console.error("Gagal delete CCTV:", error);
|
||||
return { success: false, message: "Terjadi kesalahan saat menghapus CCTV" };
|
||||
}
|
||||
};
|
||||
|
||||
export default cctvDelete;
|
||||
47
src/app/api/[[...slugs]]/_lib/keamanan/cctv/findMany.ts
Normal file
47
src/app/api/[[...slugs]]/_lib/keamanan/cctv/findMany.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function cctvFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ kode: { contains: search, mode: "insensitive" } },
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ lokasi: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.cctvKeamanan.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { kode: "asc" },
|
||||
}),
|
||||
prisma.cctvKeamanan.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil data CCTV",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di cctvFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data CCTV" };
|
||||
}
|
||||
}
|
||||
|
||||
export default cctvFindMany;
|
||||
16
src/app/api/[[...slugs]]/_lib/keamanan/cctv/findUnique.ts
Normal file
16
src/app/api/[[...slugs]]/_lib/keamanan/cctv/findUnique.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
const cctvFindUnique = async (context: Context) => {
|
||||
const id = context.params.id as string;
|
||||
|
||||
const data = await prisma.cctvKeamanan.findUnique({ where: { id } });
|
||||
|
||||
if (!data) {
|
||||
return { success: false, message: "CCTV tidak ditemukan" };
|
||||
}
|
||||
|
||||
return { success: true, data };
|
||||
};
|
||||
|
||||
export default cctvFindUnique;
|
||||
40
src/app/api/[[...slugs]]/_lib/keamanan/cctv/index.ts
Normal file
40
src/app/api/[[...slugs]]/_lib/keamanan/cctv/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import cctvCreate from "./create";
|
||||
import cctvFindMany from "./findMany";
|
||||
import cctvFindUnique from "./findUnique";
|
||||
import cctvUpdate from "./updt";
|
||||
import cctvDelete from "./del";
|
||||
import cctvStats from "./stats";
|
||||
|
||||
const CctvKeamanan = new Elysia({
|
||||
prefix: "cctv",
|
||||
tags: ["Keamanan/CCTV"],
|
||||
})
|
||||
.get("/stats", cctvStats)
|
||||
.get("/find-many", cctvFindMany)
|
||||
.get("/:id", cctvFindUnique)
|
||||
.post("/create", cctvCreate, {
|
||||
body: t.Object({
|
||||
kode: t.String(),
|
||||
nama: t.String(),
|
||||
lokasi: t.String(),
|
||||
latitude: t.Optional(t.Number()),
|
||||
longitude: t.Optional(t.Number()),
|
||||
status: t.Optional(t.Union([t.Literal("Online"), t.Literal("Offline")])),
|
||||
lastActive: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.put("/:id", cctvUpdate, {
|
||||
body: t.Object({
|
||||
kode: t.String(),
|
||||
nama: t.String(),
|
||||
lokasi: t.String(),
|
||||
latitude: t.Optional(t.Number()),
|
||||
longitude: t.Optional(t.Number()),
|
||||
status: t.Union([t.Literal("Online"), t.Literal("Offline")]),
|
||||
lastActive: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", cctvDelete);
|
||||
|
||||
export default CctvKeamanan;
|
||||
32
src/app/api/[[...slugs]]/_lib/keamanan/cctv/stats.ts
Normal file
32
src/app/api/[[...slugs]]/_lib/keamanan/cctv/stats.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const cctvStats = async () => {
|
||||
const now = new Date();
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(now.getDate() - now.getDay());
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
|
||||
try {
|
||||
const [cctvOnline, laporanMingguIni] = await Promise.all([
|
||||
prisma.cctvKeamanan.count({
|
||||
where: { isActive: true, status: "Online" },
|
||||
}),
|
||||
prisma.laporanPublik.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
tanggalWaktu: { gte: startOfWeek },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { cctvOnline, laporanMingguIni },
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Gagal ambil stats keamanan:", e);
|
||||
return { success: false, message: "Gagal mengambil statistik keamanan" };
|
||||
}
|
||||
};
|
||||
|
||||
export default cctvStats;
|
||||
40
src/app/api/[[...slugs]]/_lib/keamanan/cctv/updt.ts
Normal file
40
src/app/api/[[...slugs]]/_lib/keamanan/cctv/updt.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type CctvUpdateInput = {
|
||||
kode: string;
|
||||
nama: string;
|
||||
lokasi: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
status: "Online" | "Offline";
|
||||
lastActive?: string;
|
||||
};
|
||||
|
||||
const cctvUpdate = async (context: Context) => {
|
||||
const id = context.params.id as string;
|
||||
const { kode, nama, lokasi, latitude, longitude, status, lastActive } =
|
||||
(await context.body) as CctvUpdateInput;
|
||||
|
||||
try {
|
||||
const data = await prisma.cctvKeamanan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
kode,
|
||||
nama,
|
||||
lokasi,
|
||||
latitude,
|
||||
longitude,
|
||||
status,
|
||||
lastActive: lastActive ? new Date(lastActive) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Gagal update CCTV:", e);
|
||||
return { success: false, message: "Gagal memperbarui CCTV" };
|
||||
}
|
||||
};
|
||||
|
||||
export default cctvUpdate;
|
||||
@@ -4,6 +4,7 @@ import PolsekTerdekat from "./polsek-terdekat";
|
||||
import PencegahanKriminalitas from "./pencegahan-kriminalitas";
|
||||
import MenuTipsKeamanan from "./tips-keamanan";
|
||||
import LaporanPublik from "./laporan-publik";
|
||||
import CctvKeamanan from "./cctv";
|
||||
|
||||
import KontakDaruratKeamanan from "./kontak-darurat-keamanan";
|
||||
import KontakItem from "./kontak-darurat-keamanan/kontak-item";
|
||||
@@ -15,6 +16,7 @@ const Keamanan = new Elysia({ prefix: "/keamanan", tags: ["Keamanan"] })
|
||||
.use(PencegahanKriminalitas)
|
||||
.use(MenuTipsKeamanan)
|
||||
.use(LaporanPublik)
|
||||
.use(CctvKeamanan)
|
||||
.use(LayananPolsek)
|
||||
.use(KontakDaruratKeamanan)
|
||||
.use(KontakItem)
|
||||
|
||||
Reference in New Issue
Block a user