Compare commits
16 Commits
amalia/16-
...
amalia/10-
| Author | SHA1 | Date | |
|---|---|---|---|
| d861a3ea86 | |||
| 2f97ce81e4 | |||
| 3c0a5639b6 | |||
| 3ce650a27d | |||
| 5efb96a92a | |||
| 93ae77d335 | |||
| 0c131b80ef | |||
| 5fd5c15394 | |||
| cb565ba0bd | |||
| 940fa5a5b7 | |||
| 0b9f07e543 | |||
| 8440374424 | |||
| eaa1a74290 | |||
| 1326338335 | |||
| d1f553ee32 | |||
| b14ae8e5ff |
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Village" ADD COLUMN "isDummy" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -51,6 +51,7 @@ model Village {
|
|||||||
name String
|
name String
|
||||||
desc String @db.Text
|
desc String @db.Text
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
isDummy Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
Group Group[]
|
Group Group[]
|
||||||
|
|||||||
59
src/app/api/auth/otp/route.ts
Normal file
59
src/app/api/auth/otp/route.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { prisma } from "@/module/_global";
|
||||||
|
import { ILogin } from "@/types";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { phone }: ILogin = await req.json();
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { phone, isActive: true },
|
||||||
|
select: { id: true, phone: true, isWithoutOTP: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Nomor telepon tidak terdaftar",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate OTP
|
||||||
|
const code = Math.floor(1000 + Math.random() * 9000);
|
||||||
|
const message = `Desa+\nMasukkan kode ini ${code} pada web app Desa+ anda. Jangan berikan pada siapapun.`;
|
||||||
|
|
||||||
|
// Send WhatsApp
|
||||||
|
try {
|
||||||
|
const resWa = await fetch(`${process.env.URL_OTP}/api/wa/send-text`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
number: user.phone,
|
||||||
|
text: message,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resWa.ok) {
|
||||||
|
console.error("WhatsApp API Error:", resWa.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("WhatsApp Fetch Error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: "Sukses",
|
||||||
|
phone: user.phone,
|
||||||
|
isWithoutOTP: user.isWithoutOTP,
|
||||||
|
id: user.id,
|
||||||
|
otp: code, // Return OTP for client-side verification (as per existing logic)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return Response.json({ message: "Internal Server Error (error: 500)", success: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
1521
src/app/api/monitoring/[[...slug]]/route.ts
Normal file
1521
src/app/api/monitoring/[[...slug]]/route.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -84,13 +84,13 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
|
|
||||||
// Hitung total kegiatan per divisi & urutkan descending, ambil top sesuai limit
|
// Hitung total kegiatan per divisi & urutkan descending, ambil top sesuai limit
|
||||||
const ranked = divisions
|
const ranked = divisions
|
||||||
.map((d) => ({
|
.map((d: any) => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
division: d.name,
|
division: d.name,
|
||||||
group: d.Group.name,
|
group: d.Group.name,
|
||||||
totalKegiatan: d._count.DivisionProject
|
totalKegiatan: d._count.DivisionProject
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.totalKegiatan - a.totalKegiatan)
|
.sort((a: any, b: any) => b.totalKegiatan - a.totalKegiatan)
|
||||||
.slice(0, maxResults);
|
.slice(0, maxResults);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -119,7 +119,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Divisi Teraktif",
|
summary: "Divisi Teraktif",
|
||||||
description: "Mendapatkan daftar divisi teraktif berdasarkan jumlah proyek pada desa tertentu.",
|
description: "Menu Beranda - Mendapatkan daftar divisi teraktif berdasarkan jumlah proyek pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
take: maxResults,
|
take: maxResults,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapped = projects.map((p) => ({
|
const mapped = projects.map((p: any) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
title: p.title,
|
title: p.title,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
@@ -226,7 +226,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Latest Projects General",
|
summary: "Latest Projects General",
|
||||||
description: "Mendapatkan daftar proyek umum terbaru dari berbagai grup pada desa tertentu.",
|
description: "Menu kinerja divisi - Mendapatkan daftar proyek umum terbaru dari berbagai grup pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -336,8 +336,8 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const todayEvents = events.filter(e => moment(e.dateStart).isSame(todayMoment, 'day')).map(mapper);
|
const todayEvents = events.filter((e: any) => moment(e.dateStart).isSame(todayMoment, 'day')).map(mapper);
|
||||||
const upcomingEvents = events.filter(e => moment(e.dateStart).isAfter(todayMoment, 'day')).map(mapper);
|
const upcomingEvents = events.filter((e: any) => moment(e.dateStart).isAfter(todayMoment, 'day')).map(mapper);
|
||||||
|
|
||||||
let data: any = {
|
let data: any = {
|
||||||
idDesa: village.id,
|
idDesa: village.id,
|
||||||
@@ -380,7 +380,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Events (Today & Upcoming)",
|
summary: "Events (Today & Upcoming)",
|
||||||
description: "Mendapatkan daftar event pada hari ini dan yang akan datang untuk semua divisi pada desa tertentu.",
|
description: "Menu beranda dan kinerja divisi - Mendapatkan daftar event pada hari ini dan yang akan datang untuk semua divisi pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -489,7 +489,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Diagram Jumlah Document",
|
summary: "Diagram Jumlah Document",
|
||||||
description: "Mendapatkan diagram jumlah document pada desa tertentu.",
|
description: "Menu kinerja divisi - Mendapatkan diagram jumlah document pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -543,7 +543,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
for (let index = 0; index < dataStatus.length; index++) {
|
for (let index = 0; index < dataStatus.length; index++) {
|
||||||
const cek = data.some((i: any) => i.status == dataStatus[index].status)
|
const cek = data.some((i: any) => i.status == dataStatus[index].status)
|
||||||
if (cek) {
|
if (cek) {
|
||||||
const find = ((Number(data.find((i: any) => i.status == dataStatus[index].status)?._count) * 100) / data.reduce((n, { _count }) => n + _count, 0)).toFixed(2)
|
const find = ((Number(data.find((i: any) => i.status == dataStatus[index].status)?._count) * 100) / data.reduce((n: any, { _count }: any) => n + _count, 0)).toFixed(2)
|
||||||
const fix = find != "100.00" ? find.substr(-2, 2) == "00" ? find.substr(0, 2) : find : "100"
|
const fix = find != "100.00" ? find.substr(-2, 2) == "00" ? find.substr(0, 2) : find : "100"
|
||||||
input = {
|
input = {
|
||||||
text: fix + '%',
|
text: fix + '%',
|
||||||
@@ -581,7 +581,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Diagram Progres Kegiatan",
|
summary: "Diagram Progres Kegiatan",
|
||||||
description: "Mendapatkan diagram progres kegiatan pada desa tertentu.",
|
description: "Menu kinerja divisi - Mendapatkan diagram progres kegiatan pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -676,7 +676,7 @@ const NocServer = new Elysia({ prefix: "/api/noc" })
|
|||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Latest Discussion",
|
summary: "Latest Discussion",
|
||||||
description: "Mendapatkan latest discussion pada desa tertentu.",
|
description: "Menu kinerja divisi - Mendapatkan latest discussion pada desa tertentu.",
|
||||||
tags: ["NOC"],
|
tags: ["NOC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
return NextResponse.json({ success: true, version: "2.1.7", tahap: "beta", update: "-api untuk dashboard noc" }, { status: 200 });
|
return NextResponse.json({ success: true, version: "2.1.9", tahap: "beta", update: "-api untuk dashboard monitoring" }, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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 });
|
return NextResponse.json({ success: false, version: "Gagal mendapatkan version, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });
|
||||||
|
|||||||
11
src/lib/formatDateTime.ts
Normal file
11
src/lib/formatDateTime.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
function formatDateTime(date: Date) {
|
||||||
|
return new Intl.DateTimeFormat('id-ID', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formatDateTime
|
||||||
38
src/lib/timeAgo.ts
Normal file
38
src/lib/timeAgo.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
function timeAgo(date: Date) {
|
||||||
|
const now = new Date();
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
const diffMs = now.getTime() - d.getTime();
|
||||||
|
const seconds = Math.floor(diffMs / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
// 🔥 cek apakah masih hari yang sama
|
||||||
|
const isToday =
|
||||||
|
now.getDate() === d.getDate() &&
|
||||||
|
now.getMonth() === d.getMonth() &&
|
||||||
|
now.getFullYear() === d.getFullYear();
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
if (seconds < 60) return `${seconds} detik lalu`;
|
||||||
|
if (minutes < 60) return `${minutes} menit lalu`;
|
||||||
|
return `${hours} jam lalu`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 kalau bukan hari ini → tampil tanggal + jam
|
||||||
|
const time = d.toLocaleTimeString("id-ID", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const datePart = d.toLocaleDateString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${time} ${datePart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default timeAgo
|
||||||
@@ -5,7 +5,6 @@ import { useFocusTrap } from "@mantine/hooks";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import ViewVerification from "../../varification/view/view_verification";
|
import ViewVerification from "../../varification/view/view_verification";
|
||||||
|
|
||||||
function ViewLogin() {
|
function ViewLogin() {
|
||||||
const focusTrapRef = useFocusTrap()
|
const focusTrapRef = useFocusTrap()
|
||||||
const textInfo = "Kami akan mengirimkan kode verifikasi melalui WhatsApp untuk mengonfirmasi nomor Anda.";
|
const textInfo = "Kami akan mengirimkan kode verifikasi melalui WhatsApp untuk mengonfirmasi nomor Anda.";
|
||||||
@@ -34,23 +33,24 @@ function ViewLogin() {
|
|||||||
})
|
})
|
||||||
const cekLogin = await cek.json()
|
const cekLogin = await cek.json()
|
||||||
if (cekLogin.success) {
|
if (cekLogin.success) {
|
||||||
const code = Math.floor(1000 + Math.random() * 9000)
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://wa.wibudev.com/code?nom=${cekLogin.phone}&text=*DARMASABA*%0A%0A
|
const res = await fetch('/api/auth/otp', {
|
||||||
JANGAN BERIKAN KODE RAHASIA ini kepada siapa pun TERMASUK PIHAK DARMASABA. Masukkan otentikasi: *${encodeURIComponent(code)}*`).then(
|
method: 'POST',
|
||||||
async (res) => {
|
headers: {
|
||||||
if (res.status == 200) {
|
'Content-Type': 'application/json'
|
||||||
setValPhone(cekLogin.phone)
|
},
|
||||||
setOTP(code)
|
body: JSON.stringify({ phone: isPhone })
|
||||||
setUser(cekLogin.id)
|
})
|
||||||
setVerif(true)
|
const data = await res.json()
|
||||||
toast.success('Kode verifikasi telah dikirim')
|
if (data.success) {
|
||||||
} else {
|
setValPhone(data.phone)
|
||||||
console.error(res.status)
|
setOTP(data.otp)
|
||||||
toast.error('Internal Server Error')
|
setUser(data.id)
|
||||||
}
|
setVerif(true)
|
||||||
}
|
toast.success('Kode verifikasi telah dikirim')
|
||||||
)
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal mengirim kode verifikasi')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
toast.error('Internal Server Error')
|
toast.error('Internal Server Error')
|
||||||
|
|||||||
@@ -15,19 +15,20 @@ export default function ViewVerification({ phone, otp, user }: IVerification) {
|
|||||||
|
|
||||||
async function onResend() {
|
async function onResend() {
|
||||||
try {
|
try {
|
||||||
const code = Math.floor(1000 + Math.random() * 9000)
|
const res = await fetch('/api/auth/otp', {
|
||||||
const res = await fetch(`https://wa.wibudev.com/code?nom=${phone}&text=*DARMASABA*%0A%0A
|
method: 'POST',
|
||||||
JANGAN BERIKAN KODE RAHASIA ini kepada siapa pun TERMASUK PIHAK DARMASABA. Masukkan otentikasi: *${encodeURIComponent(code)}*`)
|
headers: {
|
||||||
.then(
|
'Content-Type': 'application/json'
|
||||||
async (res) => {
|
},
|
||||||
if (res.status == 200) {
|
body: JSON.stringify({ phone })
|
||||||
toast.success('Kode verifikasi telah dikirim')
|
})
|
||||||
setOTP(code)
|
const data = await res.json()
|
||||||
} else {
|
if (data.success) {
|
||||||
toast.error('Internal Server Error')
|
toast.success('Kode verifikasi telah dikirim')
|
||||||
}
|
setOTP(data.otp)
|
||||||
}
|
} else {
|
||||||
);
|
toast.error(data.message || 'Gagal mengirim ulang kode')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
toast.error('Internal Server Error')
|
toast.error('Internal Server Error')
|
||||||
|
|||||||
Reference in New Issue
Block a user