diff --git a/next.config.js b/next.config.js
index b866ef000ee2682cb81ded2ad967f6c160822bbb..108fc63fb16c3b2d7b0b7fc41541e77ae37488ee 100644
--- a/next.config.js
+++ b/next.config.js
@@ -3,7 +3,7 @@ const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
- // disable: process.env.NODE_ENV === 'development', // opsional
+ disable: process.env.NODE_ENV === 'development', // opsional
});
/** @type {import('next').NextConfig} */
diff --git a/package-lock.json b/package-lock.json
index 595db5a15c82a7ca2f0974000ead8e9df641aa35..a8d9c1b2ea56ca0e8db95612c050b1f8d20768aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@vladmandic/face-api": "^1.7.15",
"antd": "^5.26.1",
"axios": "^1.10.0",
+ "file-saver": "^2.0.5",
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.517.0",
@@ -26,6 +27,7 @@
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-webcam": "^7.2.0",
+ "xlsx": "^0.18.5",
"zustand": "^5.0.5"
},
"devDependencies": {
@@ -2819,6 +2821,15 @@
"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": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3399,6 +3410,19 @@
],
"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": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3489,6 +3513,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"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": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3618,6 +3651,18 @@
"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": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4221,6 +4266,12 @@
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -4361,6 +4412,15 @@
"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": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -7684,6 +7744,18 @@
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -8837,6 +8909,24 @@
"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": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz",
@@ -9211,6 +9301,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/package.json b/package.json
index d20af8b97ccfddb5ecd0f3ccc1484c7090c50483..e71cf475b3de369307291987da9ce004bf1dc014 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@vladmandic/face-api": "^1.7.15",
"antd": "^5.26.1",
"axios": "^1.10.0",
+ "file-saver": "^2.0.5",
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.517.0",
@@ -27,6 +28,7 @@
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-webcam": "^7.2.0",
+ "xlsx": "^0.18.5",
"zustand": "^5.0.5"
},
"devDependencies": {
diff --git a/public/assets/profill.png b/public/assets/profill.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5da2c05a201d3103689c0180de2c155935b0be6
Binary files /dev/null and b/public/assets/profill.png differ
diff --git a/src/app/attendance/detail/page.js b/src/app/attendance/detail/page.js
index bde04b5227c8f37fc8aa55450f7de76b7797200b..d67603fd1e8bbdbda93327aa607fc7936b6131ab 100644
--- a/src/app/attendance/detail/page.js
+++ b/src/app/attendance/detail/page.js
@@ -33,7 +33,6 @@ export default function AbsensiDetail() {
});
}
- console.log(presenceData, 'data')
if (loading) {
return ;
}
diff --git a/src/app/attendance/page.js b/src/app/attendance/page.js
index fbf2b5eeff5255a5fd7feddab8bc11cb2f07c564..75e73857ef0485cae6045d0c36c265f2dc3ef459 100644
--- a/src/app/attendance/page.js
+++ b/src/app/attendance/page.js
@@ -3,25 +3,30 @@
import DetailPresenceCardComponent from "@/components/DetailPresenceCardComponent";
import LoadingComponent from "@/components/LoadingComponent";
import axiosInstance from "@/lib/axios";
+import { DatePicker } from "antd";
import { useSession } from "next-auth/react"
import { useEffect, useState } from "react";
+import dayjs from "dayjs";
+import LoadComponent from "@/components/LoadComponent";
+import ExportExcelButton from "@/components/ExportExcelButton";
export default function attendance(){
const {data : session} = useSession()
const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
+ const [selectedMonth, setSelectedMonth] = useState(dayjs());
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
useEffect(() => {
if (session?.accessToken) {
- getDataAbsenHistory(session.accessToken);
+ getDataAbsenHistory(session.accessToken, 'all');
}
- }, [session]);
+ }, [session, selectedMonth]);
- const getDataAbsenHistory = async (token) => {
+ const getDataAbsenHistory = async (token, date) => {
setLoadingAbsenHistory(true);
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: {
Authorization: `Bearer ${token}`,
},
@@ -35,20 +40,37 @@ export default function attendance(){
}
setLoadingAbsenHistory(false);
};
+
+ const onChange = (date, dateString) => {
+ setSelectedMonth(date);
+ };
return(
-
+
Kehadiran
- {loadingAbsenHistory ? <>
> : dataAbsenHistory.map((item) => (
-
- ))}
+
+
+
+
+
+
+ {loadingAbsenHistory ?
+
+
+ div> :
+ dataAbsenHistory.map((item) => (
+
+ ))}
)
diff --git a/src/app/beranda/page.js b/src/app/beranda/page.js
index 1905f4d93400db1847b3635a7824e6e0302c6163..89e2758ffae1c9ed68aad1fe941ddd163527ddd5 100644
--- a/src/app/beranda/page.js
+++ b/src/app/beranda/page.js
@@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react';
import dayjs from 'dayjs';
import 'dayjs/locale/id';
import { useRouter } from 'next/navigation';
+import LoadComponent from '@/components/LoadComponent';
dayjs.locale('id');
@@ -18,6 +19,7 @@ export default function Beranda() {
const [dataAbsenToday, setDataAbsenToday] = useState([]);
const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
+ const [loadingDetail, setLoadingDetail] = useState();
const [loadingAbsenToday, setLoadingAbsenToday] = useState(true);
const [typePresenceIn, setTypePresenceIn] = useState('Datang');
const [typePresenceOut, setTypePresenceOut] = useState('Pulang');
@@ -100,6 +102,12 @@ export default function Beranda() {
router.push(targetRoute);
};
+ const handleDetailClick = (item) => {
+ setLoadingDetail(true);
+ console.log(item)
+ // router.push(`/check-out-telat/${presences[0]?.id}`);
+ }
+
return (
@@ -152,11 +160,14 @@ export default function Beranda() {
- {loadingAbsenHistory ? <>> : dataAbsenHistory.map((item) => (
+ {loadingAbsenHistory ? <>> :
+ loadingDetail ? <>> :
+ dataAbsenHistory.map((item) => (
handleDetailClick(item)}
/>
))}
diff --git a/src/app/check-in/page.js b/src/app/check-in/page.js
index ddcc64e5f4941cfe24efb4bd557a4cf4094cc2d0..7b0043d8c1053de03ebc9175c56afd0e7f0bf4cd 100644
--- a/src/app/check-in/page.js
+++ b/src/app/check-in/page.js
@@ -26,6 +26,7 @@ export default function CheckIn() {
const [userLocationName, setUserLocationName] = useState('');
const [refreshingLocation, setRefreshingLocation] = useState(false);
const [loadingLocation, setLoadingLocation] = useState(false);
+ const [loadingSendData, setLoadingSendData] = useState(false);
const [dataAbsen, setDataAbsen] = useState({
@@ -170,6 +171,7 @@ export default function CheckIn() {
};
const handleSendData = () => {
+ setLoadingSendData(true);
if (!userLat || !userLng ) {
messageApi.warning('Lokasi pengguna tidak tersedia');
return;
@@ -198,7 +200,6 @@ export default function CheckIn() {
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 {
@@ -293,7 +294,7 @@ export default function CheckIn() {
)}
-
diff --git a/src/app/foto/page.js b/src/app/foto/page.js
index 82ad83611f3bcd99718e273512a0198ce75a3d56..3039313ec45d25f39d339d9d5134af44f94a1eb0 100644
--- a/src/app/foto/page.js
+++ b/src/app/foto/page.js
@@ -11,9 +11,10 @@ export default function FotoPage() {
const [dataAbsen, setDataAbsen] = useState(null);
const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
- const router = useRouter();
const [absenTime, setAbsenTime] = useState(null);
+ const [loading, setLoading] = useState(false);
const { data: session } = useSession();
+ const router = useRouter();
useEffect(() => {
@@ -51,6 +52,7 @@ export default function FotoPage() {
};
const handleSubmit = async () => {
+ setLoading(true);
try {
const blob = dataURLtoBlob(imageSrc);
const formData = new FormData();
@@ -108,7 +110,7 @@ export default function FotoPage() {
{imageSrc && (
-
+
Kirim Presensi
)}
diff --git a/src/app/login/page.js b/src/app/login/page.js
index 1beb248b56b68da7b87d77b4547334c8a1f2dafc..359daa81b8760c763242c150249325da7a141e99 100644
--- a/src/app/login/page.js
+++ b/src/app/login/page.js
@@ -1,7 +1,7 @@
'use client'
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 { useRouter, useSearchParams } from 'next/navigation';
@@ -11,6 +11,15 @@ function LoginForm() {
const error = searchParams.get('error');
const [loading, setLoading] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
+ const [show, context] = notification.useNotification();
+ const [notificationShown, setNotificationShown] = useState(false);
+
+ useEffect(() => {
+ if (!notificationShown) {
+ openNotification();
+ setNotificationShown(true);
+ }
+ }, [notificationShown]);
useEffect(() => {
if (error) {
@@ -31,13 +40,26 @@ function LoginForm() {
router.push("/beranda");
} else {
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 (
{contextHolder}
+ {context}
@@ -62,7 +84,7 @@ function LoginForm() {
- Submit
+ Login
diff --git a/src/components/DetailPresenceCardComponent.js b/src/components/DetailPresenceCardComponent.js
index 528ee8df6bada6edcb6fb427a2299a6a873efeaa..694a21b404f750115a09bfed6bf095bb5f9bbcca 100644
--- a/src/components/DetailPresenceCardComponent.js
+++ b/src/components/DetailPresenceCardComponent.js
@@ -6,10 +6,11 @@ import {useRouter} from 'next/navigation';
import usePresenceStore from '@/stores/usePresenceStore';
import LoadingComponent from './LoadingComponent';
import moment from "moment";
+import LoadComponent from './LoadComponent';
const { Meta } = Card;
-export default function DetailPresenceCardComponent({ date, presences }) {
+export default function DetailPresenceCardComponent({ date, presences, onClick }) {
const router = useRouter();
const setPresenceData = usePresenceStore((state) => state.setPresenceData);
const [loading, setLoading] = useState(false);
@@ -34,7 +35,6 @@ export default function DetailPresenceCardComponent({ date, presences }) {
setLoading(true);
setPresenceData({ date, presences });
router.push('/attendance/detail');
- setLoading(false);
}
const checkDayWork = (date, presences) => {
@@ -67,10 +67,6 @@ export default function DetailPresenceCardComponent({ date, presences }) {
return (
- {loading ?
- <>
-
- > :
-
+ {loading ? <>
> :
+ <>
+
{formatTanggalIndo(date)}
@@ -124,10 +122,13 @@ export default function DetailPresenceCardComponent({ date, presences }) {
+ >
+ }
+
- }
+ {/* } */}
);
diff --git a/src/components/ExportExcelButton.js b/src/components/ExportExcelButton.js
new file mode 100644
index 0000000000000000000000000000000000000000..fddfc808bcaf6982530632aa0608785ae73a1d68
--- /dev/null
+++ b/src/components/ExportExcelButton.js
@@ -0,0 +1,114 @@
+'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 (
+
+ Export ke Excel
+
+ );
+};
+
+export default ExportExcelButton;
diff --git a/src/components/LoadComponent.js b/src/components/LoadComponent.js
new file mode 100644
index 0000000000000000000000000000000000000000..12b302a6df6ad2c75cf742450ae140c93f988ea3
--- /dev/null
+++ b/src/components/LoadComponent.js
@@ -0,0 +1,10 @@
+import { LoadingOutlined } from '@ant-design/icons';
+import { Skeleton, Spin } from 'antd';
+
+export default function LoadComponent() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/ProfileCardComponent.js b/src/components/ProfileCardComponent.js
index 6caadb2bfa1be88036a405d5d0d6aa0a1c788a08..b64d2dd3e60f1c52012e2fb9da943f3adf665c5d 100644
--- a/src/components/ProfileCardComponent.js
+++ b/src/components/ProfileCardComponent.js
@@ -15,7 +15,7 @@ export default function ProfileCardComponent({ name, image }) {
{session?.user?.name || '-'}
{session?.nip || 'NIP'}
-
+
);