Commit 4a689116 authored by jorke's avatar jorke

Initial commit

parent d6f339ec
...@@ -3,7 +3,7 @@ const withPWA = require('next-pwa')({ ...@@ -3,7 +3,7 @@ const withPWA = require('next-pwa')({
dest: 'public', dest: 'public',
register: true, register: true,
skipWaiting: true, skipWaiting: true,
// disable: process.env.NODE_ENV === 'development', // opsional disable: process.env.NODE_ENV === 'development', // opsional
}); });
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"@vladmandic/face-api": "^1.7.15", "@vladmandic/face-api": "^1.7.15",
"antd": "^5.26.1", "antd": "^5.26.1",
"axios": "^1.10.0", "axios": "^1.10.0",
"file-saver": "^2.0.5",
"geolib": "^3.3.4", "geolib": "^3.3.4",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.517.0", "lucide-react": "^0.517.0",
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-webcam": "^7.2.0", "react-webcam": "^7.2.0",
"xlsx": "^0.18.5",
"zustand": "^5.0.5" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
...@@ -2819,6 +2821,15 @@ ...@@ -2819,6 +2821,15 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
...@@ -3399,6 +3410,19 @@ ...@@ -3399,6 +3410,19 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
...@@ -3489,6 +3513,15 @@ ...@@ -3489,6 +3513,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color": { "node_modules/color": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
...@@ -3618,6 +3651,18 @@ ...@@ -3618,6 +3651,18 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
...@@ -4221,6 +4266,12 @@ ...@@ -4221,6 +4266,12 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
...@@ -4361,6 +4412,15 @@ ...@@ -4361,6 +4412,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
...@@ -7684,6 +7744,18 @@ ...@@ -7684,6 +7744,18 @@
"deprecated": "Please use @jridgewell/sourcemap-codec instead", "deprecated": "Please use @jridgewell/sourcemap-codec instead",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
...@@ -8837,6 +8909,24 @@ ...@@ -8837,6 +8909,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/workbox-background-sync": { "node_modules/workbox-background-sync": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz",
...@@ -9211,6 +9301,27 @@ ...@@ -9211,6 +9301,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"@vladmandic/face-api": "^1.7.15", "@vladmandic/face-api": "^1.7.15",
"antd": "^5.26.1", "antd": "^5.26.1",
"axios": "^1.10.0", "axios": "^1.10.0",
"file-saver": "^2.0.5",
"geolib": "^3.3.4", "geolib": "^3.3.4",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.517.0", "lucide-react": "^0.517.0",
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-webcam": "^7.2.0", "react-webcam": "^7.2.0",
"xlsx": "^0.18.5",
"zustand": "^5.0.5" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -33,7 +33,6 @@ export default function AbsensiDetail() { ...@@ -33,7 +33,6 @@ export default function AbsensiDetail() {
}); });
} }
console.log(presenceData, 'data')
if (loading) { if (loading) {
return <LoadingComponent />; return <LoadingComponent />;
} }
......
...@@ -3,25 +3,30 @@ ...@@ -3,25 +3,30 @@
import DetailPresenceCardComponent from "@/components/DetailPresenceCardComponent"; import DetailPresenceCardComponent from "@/components/DetailPresenceCardComponent";
import LoadingComponent from "@/components/LoadingComponent"; import LoadingComponent from "@/components/LoadingComponent";
import axiosInstance from "@/lib/axios"; import axiosInstance from "@/lib/axios";
import { DatePicker } from "antd";
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import dayjs from "dayjs";
import LoadComponent from "@/components/LoadComponent";
import ExportExcelButton from "@/components/ExportExcelButton";
export default function attendance(){ export default function attendance(){
const {data : session} = useSession() const {data : session} = useSession()
const [dataAbsenHistory, setDataAbsenHistory] = useState([]); const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
const [selectedMonth, setSelectedMonth] = useState(dayjs());
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true); const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
useEffect(() => { useEffect(() => {
if (session?.accessToken) { if (session?.accessToken) {
getDataAbsenHistory(session.accessToken); getDataAbsenHistory(session.accessToken, 'all');
} }
}, [session]); }, [session, selectedMonth]);
const getDataAbsenHistory = async (token) => { const getDataAbsenHistory = async (token, date) => {
setLoadingAbsenHistory(true); setLoadingAbsenHistory(true);
try { try {
const response = await axiosInstance.get('/user/get-absen-history-parent?limit=31&page=1', { const response = await axiosInstance.get(`/user/get-absen-history-parent?limit=31&page=1&monthyear=${date}`, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
...@@ -36,13 +41,30 @@ export default function attendance(){ ...@@ -36,13 +41,30 @@ export default function attendance(){
setLoadingAbsenHistory(false); setLoadingAbsenHistory(false);
}; };
const onChange = (date, dateString) => {
setSelectedMonth(date);
};
return( return(
<div className="min-h-screen"> <div className="min-h-screen pb-20">
<div className="p-4 card-head"> <div className="p-4 card-head">
Kehadiran Kehadiran
</div> </div>
<div className="p-4"> <div className="p-4">
{loadingAbsenHistory ? <><LoadingComponent /></> : dataAbsenHistory.map((item) => (
<DatePicker onChange={onChange} picker="month"
format={"MMMM YYYY"}
placeholder="Pilih Bulan" value={selectedMonth}
/>
<div className="mt-4">
<ExportExcelButton data={dataAbsenHistory}/>
</div>
{loadingAbsenHistory ?
<div className="mt-4">
<LoadComponent />
</ div> :
dataAbsenHistory.map((item) => (
<DetailPresenceCardComponent <DetailPresenceCardComponent
key={item.id} key={item.id}
date={item.date} date={item.date}
......
...@@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react'; ...@@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/id'; import 'dayjs/locale/id';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import LoadComponent from '@/components/LoadComponent';
dayjs.locale('id'); dayjs.locale('id');
...@@ -18,6 +19,7 @@ export default function Beranda() { ...@@ -18,6 +19,7 @@ export default function Beranda() {
const [dataAbsenToday, setDataAbsenToday] = useState([]); const [dataAbsenToday, setDataAbsenToday] = useState([]);
const [dataAbsenHistory, setDataAbsenHistory] = useState([]); const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true); const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
const [loadingDetail, setLoadingDetail] = useState();
const [loadingAbsenToday, setLoadingAbsenToday] = useState(true); const [loadingAbsenToday, setLoadingAbsenToday] = useState(true);
const [typePresenceIn, setTypePresenceIn] = useState('Datang'); const [typePresenceIn, setTypePresenceIn] = useState('Datang');
const [typePresenceOut, setTypePresenceOut] = useState('Pulang'); const [typePresenceOut, setTypePresenceOut] = useState('Pulang');
...@@ -100,6 +102,12 @@ export default function Beranda() { ...@@ -100,6 +102,12 @@ export default function Beranda() {
router.push(targetRoute); router.push(targetRoute);
}; };
const handleDetailClick = (item) => {
setLoadingDetail(true);
console.log(item)
// router.push(`/check-out-telat/${presences[0]?.id}`);
}
return ( return (
...@@ -152,11 +160,14 @@ export default function Beranda() { ...@@ -152,11 +160,14 @@ export default function Beranda() {
</div> </div>
</div> </div>
<div> <div>
{loadingAbsenHistory ? <><LoadingComponent /></> : dataAbsenHistory.map((item) => ( {loadingAbsenHistory ? <><LoadingComponent /></> :
loadingDetail ? <><LoadComponent /></> :
dataAbsenHistory.map((item) => (
<DetailPresenceCardComponent <DetailPresenceCardComponent
key={item.id} key={item.id}
date={item.date} date={item.date}
presences={item.presences} presences={item.presences}
onClick={() => handleDetailClick(item)}
/> />
))} ))}
</div> </div>
......
...@@ -26,6 +26,7 @@ export default function CheckIn() { ...@@ -26,6 +26,7 @@ export default function CheckIn() {
const [userLocationName, setUserLocationName] = useState(''); const [userLocationName, setUserLocationName] = useState('');
const [refreshingLocation, setRefreshingLocation] = useState(false); const [refreshingLocation, setRefreshingLocation] = useState(false);
const [loadingLocation, setLoadingLocation] = useState(false); const [loadingLocation, setLoadingLocation] = useState(false);
const [loadingSendData, setLoadingSendData] = useState(false);
const [dataAbsen, setDataAbsen] = useState({ const [dataAbsen, setDataAbsen] = useState({
...@@ -170,6 +171,7 @@ export default function CheckIn() { ...@@ -170,6 +171,7 @@ export default function CheckIn() {
}; };
const handleSendData = () => { const handleSendData = () => {
setLoadingSendData(true);
if (!userLat || !userLng ) { if (!userLat || !userLng ) {
messageApi.warning('Lokasi pengguna tidak tersedia'); messageApi.warning('Lokasi pengguna tidak tersedia');
return; return;
...@@ -198,7 +200,6 @@ export default function CheckIn() { ...@@ -198,7 +200,6 @@ export default function CheckIn() {
if (inRadius) { if (inRadius) {
messageApi.success('Anda berada di dalam radius. Data siap dikirim.'); messageApi.success('Anda berada di dalam radius. Data siap dikirim.');
console.log('dataSend', dataAbsen);
localStorage.setItem('pendingAbsensi', JSON.stringify(dataAbsen)); localStorage.setItem('pendingAbsensi', JSON.stringify(dataAbsen));
window.location.href = '/foto'; window.location.href = '/foto';
} else { } else {
...@@ -293,7 +294,7 @@ export default function CheckIn() { ...@@ -293,7 +294,7 @@ export default function CheckIn() {
)} )}
<div className="mt-4"> <div className="mt-4">
<Button type="primary" block onClick={handleSendData} disabled={!isCanCheckin}> <Button type="primary" block loading={loadingSendData} onClick={handleSendData} disabled={!isCanCheckin}>
DATANG DATANG
</Button> </Button>
</div> </div>
......
...@@ -11,9 +11,10 @@ export default function FotoPage() { ...@@ -11,9 +11,10 @@ export default function FotoPage() {
const [dataAbsen, setDataAbsen] = useState(null); const [dataAbsen, setDataAbsen] = useState(null);
const [imageSrc, setImageSrc] = useState(null); const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const router = useRouter();
const [absenTime, setAbsenTime] = useState(null); const [absenTime, setAbsenTime] = useState(null);
const [loading, setLoading] = useState(false);
const { data: session } = useSession(); const { data: session } = useSession();
const router = useRouter();
useEffect(() => { useEffect(() => {
...@@ -51,6 +52,7 @@ export default function FotoPage() { ...@@ -51,6 +52,7 @@ export default function FotoPage() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true);
try { try {
const blob = dataURLtoBlob(imageSrc); const blob = dataURLtoBlob(imageSrc);
const formData = new FormData(); const formData = new FormData();
...@@ -108,7 +110,7 @@ export default function FotoPage() { ...@@ -108,7 +110,7 @@ export default function FotoPage() {
</div> </div>
{imageSrc && ( {imageSrc && (
<Button type="primary" block className="mt-4" onClick={handleSubmit}> <Button type="primary" block className="mt-4" loading={loading} onClick={handleSubmit}>
Kirim Presensi Kirim Presensi
</Button> </Button>
)} )}
......
'use client' 'use client'
import React, { Suspense,useEffect, useState } from 'react'; import React, { Suspense,useEffect, useState } from 'react';
import { Button, Form, Image, Input, message } from 'antd'; import { Button, Form, Image, Input, message, notification } from 'antd';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
...@@ -11,6 +11,15 @@ function LoginForm() { ...@@ -11,6 +11,15 @@ function LoginForm() {
const error = searchParams.get('error'); const error = searchParams.get('error');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const [show, context] = notification.useNotification();
const [notificationShown, setNotificationShown] = useState(false);
useEffect(() => {
if (!notificationShown) {
openNotification();
setNotificationShown(true);
}
}, [notificationShown]);
useEffect(() => { useEffect(() => {
if (error) { if (error) {
...@@ -31,13 +40,26 @@ function LoginForm() { ...@@ -31,13 +40,26 @@ function LoginForm() {
router.push("/beranda"); router.push("/beranda");
} else { } else {
messageApi.error("Login Gagal, Cek kembali username dan password"); messageApi.error("Login Gagal, Cek kembali username dan password");
}
setLoading(false); setLoading(false);
}
};
const pesan =
"Apabila Anda mengalami kendala pada aplikasi, mohon untuk menghubungi layanan helpdesk IT.";
const openNotification = () => {
show.open({
type: "info",
message: "Informasi Penting",
description: pesan,
duration: 5,
});
}; };
return ( return (
<div> <div>
{contextHolder} {contextHolder}
{context}
<div className="min-h-screen flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full p-6"> <div className="max-w-md w-full p-6">
<Image preview={false} src='/assets/logo-login-new.svg' /> <Image preview={false} src='/assets/logo-login-new.svg' />
...@@ -62,7 +84,7 @@ function LoginForm() { ...@@ -62,7 +84,7 @@ function LoginForm() {
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}> <Button type="primary" htmlType="submit" block loading={loading}>
Submit Login
</Button> </Button>
</Form.Item> </Form.Item>
<Button type="primary" block disabled> <Button type="primary" block disabled>
......
...@@ -6,10 +6,11 @@ import {useRouter} from 'next/navigation'; ...@@ -6,10 +6,11 @@ import {useRouter} from 'next/navigation';
import usePresenceStore from '@/stores/usePresenceStore'; import usePresenceStore from '@/stores/usePresenceStore';
import LoadingComponent from './LoadingComponent'; import LoadingComponent from './LoadingComponent';
import moment from "moment"; import moment from "moment";
import LoadComponent from './LoadComponent';
const { Meta } = Card; const { Meta } = Card;
export default function DetailPresenceCardComponent({ date, presences }) { export default function DetailPresenceCardComponent({ date, presences, onClick }) {
const router = useRouter(); const router = useRouter();
const setPresenceData = usePresenceStore((state) => state.setPresenceData); const setPresenceData = usePresenceStore((state) => state.setPresenceData);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
...@@ -34,7 +35,6 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -34,7 +35,6 @@ export default function DetailPresenceCardComponent({ date, presences }) {
setLoading(true); setLoading(true);
setPresenceData({ date, presences }); setPresenceData({ date, presences });
router.push('/attendance/detail'); router.push('/attendance/detail');
setLoading(false);
} }
const checkDayWork = (date, presences) => { const checkDayWork = (date, presences) => {
...@@ -67,10 +67,6 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -67,10 +67,6 @@ export default function DetailPresenceCardComponent({ date, presences }) {
return ( return (
<div> <div>
{loading ?
<>
<LoadingComponent />
</> :
<div className='mt-4' > <div className='mt-4' >
<Badge.Ribbon text={getDayName(date)} <Badge.Ribbon text={getDayName(date)}
style={checkDayWork(date, presences) === 'Hari Kerja' ? style={checkDayWork(date, presences) === 'Hari Kerja' ?
...@@ -80,6 +76,8 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -80,6 +76,8 @@ export default function DetailPresenceCardComponent({ date, presences }) {
> >
<div className='card-shadow cursor-pointer hover:shadow-lg transition' <div className='card-shadow cursor-pointer hover:shadow-lg transition'
onClick={handleClick}> onClick={handleClick}>
{loading ? <><LoadComponent/></> :
<>
<div className='font-bold'> <div className='font-bold'>
{formatTanggalIndo(date)} {formatTanggalIndo(date)}
</div> </div>
...@@ -124,10 +122,13 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -124,10 +122,13 @@ export default function DetailPresenceCardComponent({ date, presences }) {
</Col> </Col>
</Row> </Row>
</div> </div>
</>
}
</div> </div>
</Badge.Ribbon> </Badge.Ribbon>
</div> </div>
} {/* } */}
</div> </div>
); );
......
'use client';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
import { Button } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/id';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
dayjs.locale('id');
dayjs.extend(isSameOrBefore);
const ExportExcelButton = ({ data }) => {
const exportToExcel = () => {
const rows = [];
// Ambil tanggal awal dan akhir dari data
const allDates = data.map((d) => d.date).sort();
const firstDate = dayjs(allDates[0]);
const lastDate = dayjs(allDates[allDates.length - 1]);
// Buat map tanggal ke data presensi
const dateMap = new Map();
data.forEach((record) => {
dateMap.set(record.date, record.presences);
});
// Iterasi dari tanggal awal ke akhir
for (
let current = firstDate;
current.isSameOrBefore(lastDate);
current = current.add(1, 'day')
) {
const isoDate = current.format('YYYY-MM-DD');
const tanggalFormat = current.format('D MMMM YYYY');
const hari = current.format('dddd');
const presences = dateMap.get(isoDate) || [];
let hasMasuk = false;
presences.forEach((presence) => {
if (presence.presence_type !== 'masuk') return;
hasMasuk = true;
const masuk = presence.time;
const pulang = presence.presence?.time || '';
let waktuKerja = '';
if (masuk && pulang) {
const masukTime = dayjs(`1970-01-01T${masuk}`);
const pulangTime = dayjs(`1970-01-01T${pulang}`);
let durasi = pulangTime.diff(masukTime, 'second');
// Kurangi istirahat berdasarkan hari
if (hari === 'Jumat') {
durasi -= 5400; // 1 jam 30 menit
} else {
durasi -= 3600; // 1 jam
}
// Pastikan durasi tidak negatif
durasi = Math.max(0, durasi);
const jam = String(Math.floor(durasi / 3600)).padStart(2, '0');
const menit = String(Math.floor((durasi % 3600) / 60)).padStart(2, '0');
const detik = String(durasi % 60).padStart(2, '0');
waktuKerja = `${jam}:${menit}:${detik}`;
}
rows.push({
Hari: hari.charAt(0).toUpperCase() + hari.slice(1),
Tanggal: tanggalFormat,
Masuk: masuk,
Pulang: pulang,
'Waktu Kerja': waktuKerja,
Keterangan:
hari === 'Sabtu' || hari === 'Minggu' ? 'Hadir (Hari Libur)' : 'Hadir',
});
});
// Jika tidak ada data masuk, tampilkan sebagai keterangan -
if (!hasMasuk) {
rows.push({
Hari: hari.charAt(0).toUpperCase() + hari.slice(1),
Tanggal: tanggalFormat,
Masuk: '',
Pulang: '',
'Waktu Kerja': '',
Keterangan: '',
});
}
}
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Presensi');
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([excelBuffer], { type: 'application/octet-stream' });
saveAs(blob, `presensi_${dayjs().format('YYYY-MM-DD')}.xlsx`);
};
return (
<Button
style={{ backgroundColor: '#DA630D', color: 'white', width: '100%' }}
className="text-12"
onClick={exportToExcel}
>
Export ke Excel
</Button>
);
};
export default ExportExcelButton;
import { LoadingOutlined } from '@ant-design/icons';
import { Skeleton, Spin } from 'antd';
export default function LoadComponent() {
return (
<div>
<Skeleton active />
</div>
);
}
...@@ -15,7 +15,7 @@ export default function ProfileCardComponent({ name, image }) { ...@@ -15,7 +15,7 @@ export default function ProfileCardComponent({ name, image }) {
<div className='font-bold '>{session?.user?.name || '-'}</div> <div className='font-bold '>{session?.user?.name || '-'}</div>
<div>{session?.nip || 'NIP'}</div> <div>{session?.nip || 'NIP'}</div>
</div> </div>
<Avatar style={{ border: '2px solid #D98324' }} size={'large'} src={"/bg/prof.jpg"} /> <Avatar style={{ border: '2px solid #D98324' }} size={'large'} src={"/assets/profill.png"} />
</div> </div>
</div> </div>
); );
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment