diff --git a/package-lock.json b/package-lock.json
index 903c4905a06547275034bc153eb13c91c13b58e5..595db5a15c82a7ca2f0974000ead8e9df641aa35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.517.0",
+ "moment": "^2.30.1",
"next": "15.3.3",
"next-auth": "^4.24.11",
"next-pwa": "^5.6.0",
@@ -5593,6 +5594,15 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/package.json b/package.json
index e80bd23a4512905f39c7be5cfd6064f909cddff1..d20af8b97ccfddb5ecd0f3ccc1484c7090c50483 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.517.0",
+ "moment": "^2.30.1",
"next": "15.3.3",
"next-auth": "^4.24.11",
"next-pwa": "^5.6.0",
diff --git a/public/assets/logo-login-new.svg b/public/assets/logo-login-new.svg
new file mode 100644
index 0000000000000000000000000000000000000000..379fdbadcd412f6caa3e7ac9c66a5a71a20a93ea
--- /dev/null
+++ b/public/assets/logo-login-new.svg
@@ -0,0 +1,32 @@
+
diff --git a/public/models/face_landmark_68_model-shard1 b/public/models/face_landmark_68_model-shard1
new file mode 100644
index 0000000000000000000000000000000000000000..fcaca474f001923870e24940a926d9f01b4ac9e9
Binary files /dev/null and b/public/models/face_landmark_68_model-shard1 differ
diff --git a/public/models/face_landmark_68_model-weights_manifest.json b/public/models/face_landmark_68_model-weights_manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..0fe27075f26c5ca65afa82254f01d85ff4fd6511
--- /dev/null
+++ b/public/models/face_landmark_68_model-weights_manifest.json
@@ -0,0 +1 @@
+[{"weights":[{"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004853619781194949,"min":-0.5872879935245888}},{"name":"dense0/conv0/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004396426443960153,"min":-0.7298067896973853}},{"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00635151559231328,"min":-0.5589333721235686}},{"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009354315552057004,"min":-1.2628325995276957}},{"name":"dense0/conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0029380727048013726,"min":-0.5846764682554731}},{"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0049374802439820535,"min":-0.6171850304977566}},{"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009941946758943446,"min":-1.3421628124573652}},{"name":"dense0/conv2/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0030300481062309416,"min":-0.5272283704841838}},{"name":"dense0/conv3/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005672684837790097,"min":-0.7431217137505026}},{"name":"dense0/conv3/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010712201455060173,"min":-1.5639814124387852}},{"name":"dense0/conv3/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0030966934035806097,"min":-0.3839899820439956}},{"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0039155554537679636,"min":-0.48161332081345953}},{"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01023082966898002,"min":-1.094698774580862}},{"name":"dense1/conv0/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0027264176630506327,"min":-0.3871513081531898}},{"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004583378632863362,"min":-0.5454220573107401}},{"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00915846403907327,"min":-1.117332612766939}},{"name":"dense1/conv1/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003091680419211294,"min":-0.5966943209077797}},{"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005407439727409214,"min":-0.708374604290607}},{"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00946493943532308,"min":-1.2399070660273235}},{"name":"dense1/conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004409168514550901,"min":-0.9788354102303}},{"name":"dense1/conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004478132958505668,"min":-0.6493292789833219}},{"name":"dense1/conv3/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011063695888893277,"min":-1.2501976354449402}},{"name":"dense1/conv3/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003909627596537272,"min":-0.6646366914113363}},{"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003213915404151468,"min":-0.3374611174359041}},{"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010917326048308728,"min":-1.4520043644250609}},{"name":"dense2/conv0/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002800439152063108,"min":-0.38085972468058266}},{"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0050568851770139206,"min":-0.6927932692509071}},{"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01074961213504567,"min":-1.3222022926106174}},{"name":"dense2/conv1/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0030654204242369708,"min":-0.5487102559384177}},{"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00591809165244009,"min":-0.917304206128214}},{"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01092823346455892,"min":-1.366029183069865}},{"name":"dense2/conv2/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002681120470458386,"min":-0.36463238398234055}},{"name":"dense2/conv3/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0048311497650894465,"min":-0.5797379718107336}},{"name":"dense2/conv3/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011227761062921263,"min":-1.4483811771168429}},{"name":"dense2/conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0034643323982463162,"min":-0.3360402426298927}},{"name":"dense3/conv0/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394978887894574,"min":-0.49227193874471326}},{"name":"dense3/conv0/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010051267287310432,"min":-1.2765109454884247}},{"name":"dense3/conv0/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003142924752889895,"min":-0.4588670139219247}},{"name":"dense3/conv1/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00448304671867221,"min":-0.5872791201460595}},{"name":"dense3/conv1/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016063522357566685,"min":-2.3613377865623026}},{"name":"dense3/conv1/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00287135781026354,"min":-0.47664539650374765}},{"name":"dense3/conv2/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006002906724518421,"min":-0.7923836876364315}},{"name":"dense3/conv2/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.017087187019048954,"min":-1.6061955797906016}},{"name":"dense3/conv2/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003124481205846749,"min":-0.46242321846531886}},{"name":"dense3/conv3/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006576311588287353,"min":-1.0193282961845398}},{"name":"dense3/conv3/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015590153955945782,"min":-1.99553970636106}},{"name":"dense3/conv3/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004453541601405424,"min":-0.6546706154065973}},{"name":"fc/weights","shape":[256,136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010417488509533453,"min":-1.500118345372817}},{"name":"fc/bias","shape":[136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0025084222648658005,"min":0.07683877646923065}}],"paths":["face_landmark_68_model-shard1"]}]
\ No newline at end of file
diff --git a/src/app/beranda/page.js b/src/app/beranda/page.js
index 88df5e26031ae7c8fdb7965c48120ec879e7e4ae..1905f4d93400db1847b3635a7824e6e0302c6163 100644
--- a/src/app/beranda/page.js
+++ b/src/app/beranda/page.js
@@ -21,6 +21,7 @@ export default function Beranda() {
const [loadingAbsenToday, setLoadingAbsenToday] = useState(true);
const [typePresenceIn, setTypePresenceIn] = useState('Datang');
const [typePresenceOut, setTypePresenceOut] = useState('Pulang');
+ const [loadingPresenceType, setLoadingPresenceType] = useState(null); // 'Datang' atau 'Pulang'
const router = useRouter();
useEffect(() => {
@@ -42,17 +43,25 @@ export default function Beranda() {
const fetchedData = response?.data ?? [];
const absenData = fetchedData.data[0];
setDataAbsenToday(fetchedData.data[0]);
- console.log(absenData);
+
+ // console.log(absenData, 'dataAbsen');
+
if (absenData?.presence_type === 'masuk') {
document.cookie = 'hasCheckedIn=true; path=/;';
} else {
- document.cookie = 'hasCheckedIn=; Max-Age=0; path=/;'; // hapus cookie jika belum absen
+ document.cookie = 'hasCheckedIn=; Max-Age=0; path=/;';
}
if (absenData?.presence?.presence_type === 'pulang') {
document.cookie = 'hasCheckedOut=true; path=/;';
} else {
- document.cookie = 'hasCheckedOut=; Max-Age=0; path=/;'; // hapus cookie jika belum absen
+ document.cookie = 'hasCheckedOut=; Max-Age=0; path=/;';
+ }
+
+ if (absenData === undefined){
+ document.cookie = 'hasAbsen=true; path=/;';
+ } else {
+ document.cookie = 'hasAbsen=; Max-Age=0; path=/;';
}
} catch (error) {
@@ -85,6 +94,12 @@ export default function Beranda() {
setLoadingAbsenHistory(false);
}
+ const handlePresenceClick = (type) => {
+ setLoadingPresenceType(type);
+ const targetRoute = type === 'Datang' ? '/check-in' : '/check-out';
+ router.push(targetRoute);
+ };
+
return (
@@ -109,8 +124,23 @@ export default function Beranda() {
-
-
+ {/*
+ */}
+ handlePresenceClick('Datang')}
+ />
+
+ handlePresenceClick('Pulang')}
+/>
diff --git a/src/app/check-out-telat/[parent_id]/page.js b/src/app/check-out-telat/[parent_id]/page.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6d9d96f887fd89a9770501f6d9d5caa663773cf
--- /dev/null
+++ b/src/app/check-out-telat/[parent_id]/page.js
@@ -0,0 +1,322 @@
+'use client'
+
+import LoadingComponent from '@/components/LoadingComponent';
+import axiosInstance from '@/lib/axios';
+import { Button, Col, message, Radio, Row } from 'antd';
+import TextArea from 'antd/es/input/TextArea';
+import { useSession } from 'next-auth/react';
+import dynamic from 'next/dynamic';
+import { use, useEffect, useState } from 'react';
+import { getLocationAddress } from '@/utils/locationUtils';
+import { ReloadOutlined } from '@ant-design/icons';
+import { useRouter } from 'next/navigation';
+
+const DynamicMap = dynamic(() => import('@/components/RealTimeMapComponent'), {
+ ssr: false,
+});
+
+export default function CheckIn({params}) {
+ const [messageApi, contextHolder] = message.useMessage();
+ const { data: session } = useSession();
+ const [dataMasterLocation, setDataMasterLocation] = useState([]);
+ const [loadingMasterLocation, setLoadingMasterLocation] = useState(true);
+ const [userLat, setUserLat] = useState(null);
+ const [userLng, setUserLng] = useState(null);
+ const [selectedLocation, setSelectedLocation] = useState(null);
+ const [radiusTarget, setRadiusTarget] = useState(0);
+ const [userLocationName, setUserLocationName] = useState('');
+ const [refreshingLocation, setRefreshingLocation] = useState(false);
+ const [loadingLocation, setLoadingLocation] = useState(false);
+ const router = useRouter();
+ const { parent_id } = use(params);
+
+
+ const [dataAbsen, setDataAbsen] = useState({
+ latitude_longitude: '',
+ presence_type: 'pulang',
+ presence_condition: '-',
+ add_information_type: '-',
+ add_information_condition: '',
+ location_validation_status: '',
+ office_id : '-',
+ office_name : '-',
+ location_name: '-',
+ parent_id: '-'
+ });
+
+ useEffect(() => {
+ if (!parent_id) {
+ alert('Data check-in tidak ditemukan');
+ } else {
+ setDataAbsen({
+ ...dataAbsen,
+ parent_id: parent_id
+ })
+ }
+ }, [parent_id]);
+
+ const refreshLocation = async () => {
+ setLoadingLocation(true);
+ if (!navigator.geolocation) {
+ messageApi.error('Geolocation tidak didukung oleh browser Anda.');
+ return;
+ }
+
+ setRefreshingLocation(true);
+
+ try {
+ const position = await new Promise((resolve, reject) => {
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
+ enableHighAccuracy: true,
+ maximumAge: 0,
+ timeout: 10000,
+ });
+ });
+
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+
+ setUserLat(lat);
+ setUserLng(lng);
+
+ const address = await getLocationAddress(lat, lng);
+ setUserLocationName(address);
+ setDataAbsen((prev) => ({
+ ...prev,
+ latitude_longitude: `${lat}, ${lng}`,
+ location_name: address || '',
+ }));
+ } catch (error) {
+ console.error('Gagal ambil lokasi:', error);
+ messageApi.error('Gagal ambil lokasi: ' + error.message);
+ } finally {
+ setRefreshingLocation(false);
+ setLoadingLocation(false);
+ }
+ };
+
+
+ console.log(parent_id);
+
+ useEffect(() => {
+ if (session?.accessToken) {
+ getMasterLocation(session.accessToken);
+ }
+ }, [session]);
+
+ useEffect(() => {
+ let watchId;
+ if (typeof window !== 'undefined' && navigator.geolocation) {
+ watchId = navigator.geolocation.watchPosition(
+ async (position) => {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ setUserLat(lat);
+ setUserLng(lng);
+
+ const address = await getLocationAddress(lat, lng);
+
+ setDataAbsen((prev) => ({
+ ...prev,
+ latitude_longitude: `${lat}, ${lng}`,
+ location_name: address || '',
+ }));
+
+ setUserLocationName(address);
+ },
+ (error) => {
+ messageApi.error('Gagal ambil lokasi: ' + error.message);
+ },
+ { enableHighAccuracy: true, maximumAge: 0, timeout: 5000 }
+ );
+ }
+ return () => {
+ if (watchId) navigator.geolocation.clearWatch(watchId);
+ };
+ }, []);
+
+ const getMasterLocation = async (token) => {
+ setLoadingMasterLocation(true);
+ try {
+ const response = await axiosInstance.get('/user/mlp', {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const fetchedData = response?.data?.data ?? [];
+ setDataMasterLocation(fetchedData.data);
+ } catch (error) {
+ messageApi.error('Gagal mengambil data Location');
+ }
+ setLoadingMasterLocation(false);
+ };
+
+ const onChangeRadio = (e) => {
+ const selected = dataMasterLocation.find((loc) => loc.name_location === e.target.value);
+
+ if (e.target.value === 'Dinas Luar') {
+ setDataAbsen({
+ ...dataAbsen,
+ presence_condition: e.target.value,
+ office_name: e.target.value,
+ office_id: '-',
+ location_validation_status: 0,
+
+ });
+ setSelectedLocation(null);
+ setRadiusTarget(0);
+ } else if (selected) {
+ setSelectedLocation(selected);
+ setRadiusTarget(selected.radius);
+ setDataAbsen({
+ ...dataAbsen,
+ presence_condition: 'WFO',
+ office_name: selected.name_location,
+ office_id: selected.id,
+ location_validation_status: 1,
+ add_information_condition: ''
+ });
+ }
+ };
+
+ const onChangeCatatan = (e) => {
+ setDataAbsen({
+ ...dataAbsen,
+ add_information_condition: e.target.value,
+ });
+ };
+
+ const handleSendData = () => {
+ if (!userLat || !userLng ) {
+ messageApi.warning('Lokasi pengguna tidak tersedia');
+ return;
+ }
+
+ if(dataAbsen.office_name !== 'Dinas Luar') {
+ const [targetLat, targetLng] = selectedLocation.latitude_longitude.split(', ').map(Number);
+
+ const R = 6371;
+ const dLat = (targetLat - userLat) * (Math.PI / 180);
+ const dLon = (targetLng - userLng) * (Math.PI / 180);
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(userLat * (Math.PI / 180)) *
+ Math.cos(targetLat * (Math.PI / 180)) *
+ Math.sin(dLon / 2) *
+ Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ const distance = R * c * 1000;
+
+ const inRadius = distance <= radiusTarget;
+ setDataAbsen((prev) => ({
+ ...prev,
+ location_validation_status: inRadius ? 1 : 0,
+ }));
+
+ if (inRadius) {
+ messageApi.success('Anda berada di dalam radius. Data siap dikirim.');
+ console.log('dataSend', dataAbsen);
+ localStorage.setItem('pendingAbsensi', JSON.stringify(dataAbsen));
+ window.location.href = '/foto';
+ } else {
+ messageApi.warning('Anda berada di luar radius kantor !');
+ }
+ } else {
+ messageApi.success('Anda berada di Luar Kantor. Data siap dikirim.');
+ console.log('dataSend', dataAbsen);
+ localStorage.setItem('pendingAbsensi', JSON.stringify(dataAbsen));
+ window.location.href = '/foto';
+ }
+
+ };
+
+ const isCanCheckin =
+ (dataAbsen.presence_condition === 'Dinas Luar' && dataAbsen.add_information_condition.length >= 10) ||
+ (dataAbsen.presence_condition === 'WFO');
+
+ return (
+
+ {contextHolder}
+
+
+
PRESENSI DATANG
+
+
+ }
+ onClick={refreshLocation}
+ loading={refreshingLocation}
+ title="Refresh Lokasi"
+ size='small'
+ />
+
+
+
+
+
+ {loadingLocation ?
+
+ Memuat lokasi...
+
:
+
+ }
+
+ {loadingMasterLocation ? (
+
+ ) : (
+ <>
+ {userLat && userLng && (
+
+ Lokasi Anda : {userLat}, {userLng}
+ Nama Tempat : {userLocationName || ' Mencari...'}
+
+ )}
+
+
LOKASI
+
+
+
+ {dataMasterLocation.map((location) => (
+
+ {location.name_location}
+
+ ))}
+
+ Dinas Luar
+
+
+
+
+
+ {dataAbsen.presence_condition === 'Dinas Luar' && (
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/app/check-out/page.js b/src/app/check-out/page.js
index b8354874e37c7025614de031866a704bfaa1c58e..385d740195e0621e8b744bd21ff673632aea1bbc 100644
--- a/src/app/check-out/page.js
+++ b/src/app/check-out/page.js
@@ -222,7 +222,7 @@ export default function CheckIn() {
{contextHolder}
-
PRESENSI DATANG
+
PRESENSI PULANG
{imageSrc && (
diff --git a/src/app/layout.js b/src/app/layout.js
index e7fb1b9c1f98d85968cd45ace7e61a6fb2b147ac..ef8193d474762ebd5063c305c20606d6ed9bfa0b 100644
--- a/src/app/layout.js
+++ b/src/app/layout.js
@@ -2,7 +2,7 @@ import '../../styles/globals.css';
import { AntdRegistry } from '@ant-design/nextjs-registry';
import React from 'react';
import { DM_Sans } from 'next/font/google'
-import { ConfigProvider } from 'antd';
+import { ConfigProvider, App } from 'antd';
import theme from '../../theme.config';
import '@ant-design/v5-patch-for-react-19';
import ConditionalBottomMenu from '@/components/ConditionalBottomMenu';
diff --git a/src/app/login/page.js b/src/app/login/page.js
index bd1af80aebafc2a9d3559cb4efe8ace4e1a0bd5d..4a644ceb670eecd3e451f335652d6042e5b62e47 100644
--- a/src/app/login/page.js
+++ b/src/app/login/page.js
@@ -1,7 +1,7 @@
'use client'
import React, { useEffect, useState } from 'react';
-import { Button, Form, Input } from 'antd';
+import { Button, Form, Image, Input, message } from 'antd';
import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -11,6 +11,7 @@ export default function Login() {
const searchParams = useSearchParams();
const error = searchParams.get('error');
const [loading, setLoading] = useState(false);
+ const [messageApi, contextHolder] = message.useMessage();
useEffect(() => {
if (error) {
@@ -27,41 +28,49 @@ export default function Login() {
});
if (result.ok) {
+ messageApi.success('Login Berhasil')
router.push("/beranda");
} else {
- alert("Login gagal");
+ messageApi.error("Login Gagal, Cek kembali username dan password");
}
setLoading(false);
};
return (
-
-
);
diff --git a/src/components/BottomMenuComponent.js b/src/components/BottomMenuComponent.js
index ad2b88b47467c8475f1dd57c6d4320de60b7c4a3..c4b57e79dc7a37f5121dbf3f7742323e2506224b 100644
--- a/src/components/BottomMenuComponent.js
+++ b/src/components/BottomMenuComponent.js
@@ -3,10 +3,13 @@
import { usePathname, useRouter } from 'next/navigation';
import { CalendarCheck, Home, User } from 'lucide-react';
import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import { Spin } from 'antd';
export default function BottomMenuComponent() {
const pathname = usePathname();
const router = useRouter();
+ const [loading, setLoading] = useState(false);
const menus = [
{
@@ -26,28 +29,49 @@ export default function BottomMenuComponent() {
},
];
+ const handleNavigation = (path) => {
+ if (pathname === path) return;
+ setLoading(true);
+ router.push(path);
+ };
+
+ // Matikan loading saat path berubah
+ useEffect(() => {
+ setLoading(false);
+ }, [pathname]);
+
return (
-
-
- {menus.map((menu) => {
- const isActive = pathname === menu.path;
-
- return (
-
- );
- })}
+ <>
+ {/* Loading Overlay */}
+ {loading && (
+
+
+
+ )}
+
+ {/* Bottom Menu */}
+
+
+ {menus.map((menu) => {
+ const isActive = pathname === menu.path;
+
+ return (
+
+ );
+ })}
+
-
+ >
);
}
diff --git a/src/components/CameraComponent.js b/src/components/CameraComponent.js
index 2e982858d9a39d2e6848786f0f827441d44aa55d..3417cc8f399b627481f5c2a68fd55e817370746e 100644
--- a/src/components/CameraComponent.js
+++ b/src/components/CameraComponent.js
@@ -1,62 +1,74 @@
'use client';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam';
import { Button, message } from 'antd';
+import { loadFaceApi } from '@/utils/loadFaceApi';
-// Fungsi kompresi gambar agar < 500KB
-export const compressImage = async (base64, maxSizeKB = 500, quality = 0.7) => {
- return new Promise((resolve) => {
- const img = new Image();
- img.src = base64;
+export default function CameraComponent({ onCapture }) {
+ const webcamRef = useRef(null);
+ const canvasRef = useRef(null);
+ const [faceDetected, setFaceDetected] = useState(false);
+ const [imageSrc, setImageSrc] = useState(null);
+ const [messageApi, contextHolder] = message.useMessage();
+ const intervalRef = useRef(null);
- img.onload = () => {
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
+ useEffect(() => {
+ initFaceDetection();
+ return () => {
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, []);
- canvas.width = img.width;
- canvas.height = img.height;
+ const initFaceDetection = async () => {
+ const faceapi = await loadFaceApi();
- // Flip secara horizontal (mirror)
- ctx.translate(canvas.width, 0);
- ctx.scale(-1, 1);
- ctx.drawImage(img, 0, 0);
+ intervalRef.current = setInterval(async () => {
+ if (
+ webcamRef.current &&
+ webcamRef.current.video &&
+ webcamRef.current.video.readyState === 4
+ ) {
+ const video = webcamRef.current.video;
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext('2d');
- let compressedBase64 = canvas.toDataURL('image/jpeg', quality);
- let sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024);
+ const displaySize = {
+ width: video.videoWidth,
+ height: video.videoHeight,
+ };
- // Loop jika masih > maxSizeKB
- while (sizeInKB > maxSizeKB && quality > 0.1) {
- quality -= 0.05;
- compressedBase64 = canvas.toDataURL('image/jpeg', quality);
- sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024);
- }
+ // Samakan ukuran canvas dengan video
+ canvas.width = displaySize.width;
+ canvas.height = displaySize.height;
- resolve(compressedBase64);
- };
- });
-};
+ const detections = await faceapi.detectAllFaces(
+ video,
+ new faceapi.TinyFaceDetectorOptions({
+ inputSize: 416,
+ scoreThreshold: 0.5,
+ })
+ );
-export default function CameraComponent({ onCapture }) {
- const webcamRef = useRef(null);
- const [imageSrc, setImageSrc] = useState(null);
- const [loading, setLoading] = useState(false);
- const [messageApi, contextHolder] = message.useMessage();
+ setFaceDetected(detections.length > 0);
+
+ const resized = faceapi.resizeResults(detections, displaySize);
- const capture = async () => {
- if (!webcamRef.current) return;
+ // Gambar bounding box mirror
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.save();
+ ctx.scale(-1, 1);
+ ctx.translate(-canvas.width, 0);
+ faceapi.draw.drawDetections(canvas, resized);
+ ctx.restore();
+ }
+ }, 400);
+ };
+ const capture = () => {
const screenshot = webcamRef.current.getScreenshot();
- if (!screenshot) {
- messageApi.error('Gagal mengambil gambar');
- return;
- }
-
- setLoading(true);
- const compressed = await compressImage(screenshot);
- setImageSrc(compressed);
- setLoading(false);
- onCapture(compressed);
+ setImageSrc(screenshot);
+ onCapture(screenshot);
};
const reset = () => {
@@ -65,18 +77,51 @@ export default function CameraComponent({ onCapture }) {
};
return (
-
+
+ {contextHolder}
+
{!imageSrc ? (
<>
-
-
- {faceDetected ? (
+ {eyesDetected ? (
Wajah terdeteksi ✅
) : (
- Arahkan wajah ke kamera ❌
+ Arahkan wajah agar terlihat ❌
)}
@@ -120,7 +153,7 @@ export default function CameraFaceComponent({ onCapture }) {
block
className="mt-4"
onClick={capture}
- disabled={!faceDetected}
+ disabled={!eyesDetected || !faceBox}
>
Ambil Foto
diff --git a/src/components/DetailPresenceCardComponent.js b/src/components/DetailPresenceCardComponent.js
index 2251d44d47a8ce75a33261e014fdf5785b6a9b61..528ee8df6bada6edcb6fb427a2299a6a873efeaa 100644
--- a/src/components/DetailPresenceCardComponent.js
+++ b/src/components/DetailPresenceCardComponent.js
@@ -1,15 +1,19 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Card, Avatar, Row, Col, Flex, Badge, Button } from 'antd';
import { CloseCircleOutlined, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { LogIn, LogOut } from 'lucide-react';
import {useRouter} from 'next/navigation';
import usePresenceStore from '@/stores/usePresenceStore';
+import LoadingComponent from './LoadingComponent';
+import moment from "moment";
const { Meta } = Card;
export default function DetailPresenceCardComponent({ date, presences }) {
const router = useRouter();
const setPresenceData = usePresenceStore((state) => state.setPresenceData);
+ const [loading, setLoading] = useState(false);
+ const [loadingCheckOutTelat, setLoadingCheckOutTelat] = useState(false);
function getDayName(dateString) {
@@ -27,11 +31,12 @@ export default function DetailPresenceCardComponent({ date, presences }) {
}
const handleClick = () => {
+ setLoading(true);
setPresenceData({ date, presences });
router.push('/attendance/detail');
+ setLoading(false);
}
-
const checkDayWork = (date, presences) => {
if (getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu'){
if (!presences[0]) {
@@ -44,38 +49,86 @@ export default function DetailPresenceCardComponent({ date, presences }) {
}
}
// getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu' ? !presences[0] ? 'Hari Libur' : 'Hari Kerja' : 'Hari Kerja'
-
+
+ const handelCheckOutDelay = (presences) => {
+ setLoadingCheckOutTelat(true);
+ router.push(`/check-out-telat/${presences[0]?.id}`);
+
+ setLoadingCheckOutTelat(false);
+
+ }
+
+ const startTime = moment(moment(presences[0]?.date).format(`YYYY MM DD ${presences[0]?.time}`), "YYYY-MM-DD HH:mm:ss");
+ // Tanggal dan waktu akhir
+ const endTime = moment(moment(), "YYYY-MM-DD HH:mm:ss");
+ // Menghitung selisih waktu
+ const duration = moment.duration(endTime.diff(startTime));
+ const hours = duration.asHours();
+
return (
-
-
-
-
- {formatTanggalIndo(date)}
-
-
-
-
-
- } style={{ background: '#EFDCAB'}} />
- {presences[0]?.time || '00:00:00'} WIB
-
-
-
-
- } style={{ background: '#EFDCAB'}} />
- {presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB
-
-
-
+
+ {loading ?
+ <>
+
+ > :
+
+
+
+
+ {formatTanggalIndo(date)}
+
+
+
+
+
+ } style={{ background: '#EFDCAB'}} />
+ {presences[0]?.time || '00:00:00'} WIB
+
+
+
+
+ } style={{ background: '#EFDCAB'}} />
+ {presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB
+
+ {moment().subtract(1, "days").format("DD MMMM YYYY") == moment(presences[0]?.date).format("DD MMMM YYYY") ?
+ presences[0]?.presence === null ?
+ hours <= 23 ?
+ // hours <= 16 ?
+
+
{
+ e.stopPropagation()
+ handelCheckOutDelay(presences)
+ }}
+ loading={loadingCheckOutTelat}
+ >
+
+ Pulang
+
+
+
+ : ""
+ : ""
+ : ""
+ }
+
+
+
-
-
+
+
+ }
+
);
}
diff --git a/src/components/PresenceCardComponent.js b/src/components/PresenceCardComponent.js
index e3e08b56f55f5c262b3b65b1bf0093b8139859bd..6d76edf8eecea4c913d004a0b8c4dd5caa0206eb 100644
--- a/src/components/PresenceCardComponent.js
+++ b/src/components/PresenceCardComponent.js
@@ -1,73 +1,48 @@
-import React, { useEffect, useState } from 'react';
-import { Card, Avatar, Row, Col, Flex, Badge, Button, Tag } from 'antd';
-import Icon, { CloseCircleOutlined, CloseCircleTwoTone, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
+import { Avatar, Button, Flex } from 'antd';
import { LogIn, LogOut } from 'lucide-react';
-import { useRouter } from 'next/navigation';
-
-const { Meta } = Card;
-
-export default function PresenceCardComponent({ presenceType, presenceUrl, presenceData }) {
- const router = useRouter();
-
- const handleClick = (type) => {
- if(type === 'Datang'){
- router.push('/check-in');
- } else {
- router.push('/check-out');
- }
- }
-
- // console.log(presenceData?.presence?.time)
-
+export default function PresenceCardComponent({ presenceType, presenceUrl, presenceData, loading, onClick }) {
return (
-
-
-
-
- {!presenceUrl ?
- presenceType === 'Datang' ?
-
} style={{ background: '#EFDCAB'}} />
- :
-
} style={{ background: '#EFDCAB'}} />
- :
-
- }
-
-
-
- {presenceType === 'Datang' ?
-
{presenceData?.time || '00:00'} WIB
- :
-
{presenceData?.presence?.time || '00:00'} WIB
- }
-
-
-
-
-
- {presenceType === "Datang"
- ?
- (
-
handleClick(presenceType)} block disabled={presenceData?.presence_type === 'masuk'}>
-
- Datang
-
-
- ) :
- (
-
handleClick(presenceType)} block disabled={presenceData?.presence?.presence_type === 'pulang' || !presenceData ? true : false}>
-
- Pulang
-
-
- )
-
- }
-
+
+
+
+
+ {!presenceUrl ? (
+ presenceType === 'Datang' ? (
+
} style={{ background: '#EFDCAB' }} />
+ ) : (
+
} style={{ background: '#EFDCAB' }} />
+ )
+ ) : (
+
+ )}
+
+
+ {presenceType === 'Datang'
+ ? `${presenceData?.time || '00:00'} WIB`
+ : `${presenceData?.presence?.time || '00:00'} WIB`}
+
+
+
+
);
}
diff --git a/src/components/ProfileCardComponent.js b/src/components/ProfileCardComponent.js
index 8499f756654c47eedd1d2d123f2cf58fd5913a87..6caadb2bfa1be88036a405d5d0d6aa0a1c788a08 100644
--- a/src/components/ProfileCardComponent.js
+++ b/src/components/ProfileCardComponent.js
@@ -13,7 +13,7 @@ export default function ProfileCardComponent({ name, image }) {
{session?.user?.name || '-'}
-
{session?.user?.nip || 'NIP'}
+
{session?.nip || 'NIP'}
diff --git a/src/middleware.js b/src/middleware.js
index 66c333d9476a4561261f35c04d5f3a89394d265c..7c00ea3acce6e839f7a7e8542efd2e5405ea808e 100644
--- a/src/middleware.js
+++ b/src/middleware.js
@@ -37,6 +37,13 @@ export async function middleware(req) {
}
}
+ if (currentPath === "/check-out") {
+ const hasAbsen = req.cookies.get("hasAbsen")?.value;
+ if (hasAbsen === "true") {
+ return NextResponse.redirect(new URL("/", req.url));
+ }
+ }
+
return NextResponse.next();
}
diff --git a/src/utils/loadFaceApi.js b/src/utils/loadFaceApi.js
index 9094fa6c0576f30539a71d07027f950721bedb3f..f85d6eb010e7c334f43f15b0aae3fa3a2592d6d5 100644
--- a/src/utils/loadFaceApi.js
+++ b/src/utils/loadFaceApi.js
@@ -5,7 +5,10 @@ export const loadFaceApi = async () => {
const mod = await import('@vladmandic/face-api');
faceapi = mod;
- await faceapi.nets.tinyFaceDetector.loadFromUri('/models');
+ await Promise.all([
+ faceapi.nets.tinyFaceDetector.loadFromUri('/models'),
+ faceapi.nets.faceLandmark68Net.loadFromUri('/models'),
+ ]);
}
return faceapi;
};
diff --git a/src/utils/locationUtils.js b/src/utils/locationUtils.js
index ab7032978ec522e4573103ec66fce648256574f8..0aef68d383644745c64aab77070c66030eff7219 100644
--- a/src/utils/locationUtils.js
+++ b/src/utils/locationUtils.js
@@ -10,12 +10,12 @@ export const getLocationAddress = async (lat, lon) => {
} else {
// Jika error dari server (misal rate limit, invalid key, dll)
const errorData = await response.json();
- console.error('Error fetching location:', errorData);
+ // console.error('Error fetching location:', errorData);
return 'Alamat tidak terdeteksi';
}
} catch (error) {
// Jika error jaringan atau timeout
- console.error('Network error:', error);
+ // console.error('Network error:', error);
return 'Alamat tidak terdeteksi';
}
};
\ No newline at end of file