Commit 5f9b580a authored by jorke's avatar jorke

Initial commit

parent fef8775c
......@@ -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",
......
<svg width="3310" height="2041" viewBox="0 0 3310 2041" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1_3988)">
<path d="M2005.3 480.986C1846.22 275.364 1510.26 275.364 1351.18 480.986C1224.57 644.644 1224.57 852.937 1351.18 1016.6C1510.26 1222.22 1846.22 1222.22 2005.3 1016.6C2131.92 852.937 2131.92 644.644 2005.3 480.986Z" fill="white"/>
<path d="M2005.3 411.174C1819.12 236.275 1530.77 229.503 1351.18 411.174C1164.03 600.5 1166.53 877.5 1351.18 1083C1507.32 1256.76 1847.45 1296.4 2079.63 991.995M1351.18 480.986C1510.26 275.364 1846.22 275.364 2005.3 480.986C2131.92 644.644 2131.92 852.937 2005.3 1016.6C1846.22 1222.22 1510.26 1222.22 1351.18 1016.6C1224.57 852.937 1224.57 644.644 1351.18 480.986Z" stroke="url(#paint0_linear_1_3988)" stroke-width="50"/>
<path d="M1890.44 571.97C1922.27 674.552 1794.77 926.309 1720.85 1082.56C1705.26 1115.52 1651.24 1116.49 1634.32 1084.19C1553.36 929.599 1415.67 685.253 1453.78 571.97C1524.1 362.939 1825.96 364.168 1890.44 571.97Z" fill="#040A44"/>
<path d="M1803.28 499.842C1730.05 426.608 1611.31 426.608 1538.08 499.842C1464.85 573.075 1464.85 691.81 1538.08 765.044C1611.31 838.277 1730.05 838.277 1803.28 765.044C1876.52 691.81 1876.52 573.075 1803.28 499.842Z" fill="url(#paint1_linear_1_3988)"/>
<path d="M1568.27 616.926L1634.47 709.02C1635.18 710.015 1636.61 710.139 1637.49 709.281L1779.74 569.934" stroke="#040A44" stroke-width="40" stroke-linecap="round"/>
<path d="M857.992 1779.5C843.159 1779.5 827.909 1778.25 812.242 1775.75C796.742 1773.42 782.159 1770.33 768.492 1766.5C754.992 1762.67 743.992 1758.67 735.492 1754.5L741.492 1682.25C752.992 1688.75 764.992 1694.33 777.492 1699C790.159 1703.5 802.742 1707 815.242 1709.5C827.909 1712 839.992 1713.25 851.492 1713.25C862.992 1713.25 872.992 1711.83 881.492 1709C889.992 1706.17 896.576 1701.92 901.242 1696.25C905.909 1690.42 908.242 1682.92 908.242 1673.75C908.242 1664.42 905.159 1656.75 898.992 1650.75C892.992 1644.58 884.326 1639.25 872.992 1634.75C861.659 1630.08 848.159 1625.5 832.492 1621C810.826 1614.67 792.326 1607.33 776.992 1599C761.659 1590.5 749.909 1579.67 741.742 1566.5C733.742 1553.17 729.742 1536.08 729.742 1515.25C729.742 1494.42 734.326 1476.08 743.492 1460.25C752.659 1444.25 766.492 1431.75 784.992 1422.75C803.492 1413.75 826.909 1409.25 855.242 1409.25C866.409 1409.25 877.576 1410 888.742 1411.5C899.909 1412.83 910.742 1414.67 921.242 1417C931.909 1419.33 941.742 1422 950.742 1425C959.742 1428 967.576 1431.17 974.242 1434.5L967.992 1507C957.326 1500.33 946.242 1494.75 934.742 1490.25C923.242 1485.75 911.992 1482.33 900.992 1480C889.992 1477.67 879.992 1476.5 870.992 1476.5C860.826 1476.5 851.576 1477.83 843.242 1480.5C835.076 1483.17 828.576 1487.25 823.742 1492.75C818.909 1498.25 816.492 1505.17 816.492 1513.5C816.492 1521.67 818.742 1528.58 823.242 1534.25C827.909 1539.75 834.992 1544.67 844.492 1549C854.159 1553.17 866.576 1557.5 881.742 1562C910.242 1570.5 932.742 1579.42 949.242 1588.75C965.742 1597.92 977.409 1609 984.242 1622C991.242 1635 994.742 1651.33 994.742 1671C994.742 1692.83 989.992 1711.92 980.492 1728.25C970.992 1744.42 956.159 1757 935.992 1766C915.826 1775 889.826 1779.5 857.992 1779.5ZM1045.29 1773C1051.62 1753.83 1058.2 1733.67 1065.04 1712.5C1072.04 1691.33 1078.62 1671.25 1084.79 1652.25L1125.04 1530C1132.2 1507.83 1138.7 1487.92 1144.54 1470.25C1150.37 1452.42 1156.2 1434.5 1162.04 1416.5H1268.04C1274.04 1435.17 1279.87 1453.33 1285.54 1471C1291.37 1488.67 1297.87 1508.33 1305.04 1530L1344.79 1652.5C1351.29 1672.17 1357.87 1692.42 1364.54 1713.25C1371.37 1734.08 1377.87 1754 1384.04 1773H1299.04C1293.7 1754 1288.2 1734.58 1282.54 1714.75C1276.87 1694.75 1271.45 1675.83 1266.29 1658L1217.29 1486.25H1211.29L1161.79 1656.25C1156.45 1674.75 1150.79 1694.17 1144.79 1714.5C1138.95 1734.67 1133.29 1754.17 1127.79 1773H1045.29ZM1131.29 1698.5L1140.04 1637H1299.29L1306.29 1698.5H1131.29ZM1454.41 1773C1454.41 1753.83 1454.41 1735.25 1454.41 1717.25C1454.41 1699.25 1454.41 1679.08 1454.41 1656.75V1535.75C1454.41 1512.75 1454.41 1492 1454.41 1473.5C1454.41 1455 1454.41 1436 1454.41 1416.5H1536.66C1536.66 1436 1536.66 1455 1536.66 1473.5C1536.66 1492 1536.66 1512.75 1536.66 1535.75V1643.5C1536.66 1665.83 1536.66 1686 1536.66 1704C1536.66 1722 1536.66 1740.58 1536.66 1759.75L1508.16 1701.25H1577.91C1595.24 1701.25 1610.41 1701.25 1623.41 1701.25C1636.58 1701.25 1648.74 1701.25 1659.91 1701.25C1671.24 1701.25 1682.83 1701.25 1694.66 1701.25V1773H1454.41ZM1759.2 1773C1761.7 1754.33 1764.12 1735.67 1766.45 1717C1768.78 1698.33 1771.28 1678.08 1773.95 1656.25L1788.7 1536.75C1791.53 1515.25 1794.12 1494.92 1796.45 1475.75C1798.78 1456.58 1801.2 1436.83 1803.7 1416.5H1886.95C1894.95 1436.83 1902.53 1456.33 1909.7 1475C1917.03 1493.5 1924.28 1511.92 1931.45 1530.25L1973.95 1639.25H1979.45L2020.7 1530.5C2027.7 1512 2034.62 1493.67 2041.45 1475.5C2048.45 1457.33 2055.78 1437.67 2063.45 1416.5H2145.7C2148.2 1436.5 2150.62 1456.08 2152.95 1475.25C2155.45 1494.42 2158.12 1515 2160.95 1537L2176.2 1657C2178.87 1678.67 2181.37 1698.67 2183.7 1717C2186.2 1735.33 2188.62 1754 2190.95 1773H2112.7C2109.7 1748 2106.78 1724.25 2103.95 1701.75C2101.12 1679.25 2098.53 1658.17 2096.2 1638.5L2085.7 1554H2080.2L2049.2 1644.75C2041.7 1667.08 2034.12 1689.25 2026.45 1711.25C2018.95 1733.08 2011.95 1753.67 2005.45 1773H1950.95C1946.28 1760.5 1941.2 1747.08 1935.7 1732.75C1930.37 1718.42 1924.87 1703.83 1919.2 1689C1913.53 1674 1907.95 1659.33 1902.45 1645L1867.95 1554H1862.45L1852.2 1638C1849.87 1657.83 1847.28 1679.08 1844.45 1701.75C1841.78 1724.25 1838.95 1748 1835.95 1773H1759.2ZM2251.63 1773C2257.97 1753.83 2264.55 1733.67 2271.38 1712.5C2278.38 1691.33 2284.97 1671.25 2291.13 1652.25L2331.38 1530C2338.55 1507.83 2345.05 1487.92 2350.88 1470.25C2356.72 1452.42 2362.55 1434.5 2368.38 1416.5H2474.38C2480.38 1435.17 2486.22 1453.33 2491.88 1471C2497.72 1488.67 2504.22 1508.33 2511.38 1530L2551.13 1652.5C2557.63 1672.17 2564.22 1692.42 2570.88 1713.25C2577.72 1734.08 2584.22 1754 2590.38 1773H2505.38C2500.05 1754 2494.55 1734.58 2488.88 1714.75C2483.22 1694.75 2477.8 1675.83 2472.63 1658L2423.63 1486.25H2417.63L2368.13 1656.25C2362.8 1674.75 2357.13 1694.17 2351.13 1714.5C2345.3 1734.67 2339.63 1754.17 2334.13 1773H2251.63ZM2337.63 1698.5L2346.38 1637H2505.63L2512.63 1698.5H2337.63Z" fill="#DA630D"/>
</g>
<defs>
<filter id="filter0_d_1_3988" x="-4" y="0" width="3318" height="2049" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_3988"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_3988" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1_3988" x1="1376.07" y1="379.609" x2="2417.47" y2="1086.88" gradientUnits="userSpaceOnUse">
<stop stop-color="#F4A549"/>
<stop offset="0.437953" stop-color="#FBAB33"/>
<stop offset="0.947917" stop-color="#DA630D"/>
</linearGradient>
<linearGradient id="paint1_linear_1_3988" x1="1552.51" y1="485.683" x2="1976.19" y2="790.065" gradientUnits="userSpaceOnUse">
<stop stop-color="#F4A549"/>
<stop offset="0.437953" stop-color="#FBAB33"/>
<stop offset="0.947917" stop-color="#DA630D"/>
</linearGradient>
</defs>
</svg>
[{"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
......@@ -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() {
</div>
</div>
<div className="grid grid-cols-2 gap-4 ">
<PresenceCard presenceType={typePresenceIn} presenceData={dataAbsenToday}/>
<PresenceCard presenceType={typePresenceOut} presenceData={dataAbsenToday}/>
{/* <PresenceCard presenceType={typePresenceIn} presenceData={dataAbsenToday}/>
<PresenceCard presenceType={typePresenceOut} presenceData={dataAbsenToday}/> */}
<PresenceCard
presenceType={typePresenceIn}
presenceData={dataAbsenToday}
presenceUrl={null}
loading={loadingPresenceType === 'Datang'}
onClick={() => handlePresenceClick('Datang')}
/>
<PresenceCard
presenceType={typePresenceOut}
presenceData={dataAbsenToday}
presenceUrl={null}
loading={loadingPresenceType === 'Pulang'}
onClick={() => handlePresenceClick('Pulang')}
/>
</div>
</div>
<div className="grid gap-4 mt-4">
......
This diff is collapsed.
......@@ -222,7 +222,7 @@ export default function CheckIn() {
{contextHolder}
<div className="p-4 card-head">
<div className="grid gap-4 mt-4">
<div className="col-start font-bold">PRESENSI DATANG</div>
<div className="col-start font-bold">PRESENSI PULANG</div>
<div className="col-end-3 text-end text-blue-600">
<div>
<Button
......
......@@ -3,7 +3,6 @@
import { useEffect, useState } from 'react';
import { Button, message } from 'antd';
import { useRouter } from 'next/navigation';
import CameraComponent from '@/components/CameraComponent';
import axiosInstance from '@/lib/axios';
import { useSession } from 'next-auth/react';
import CameraFaceComponent from '@/components/CameraFaceComponent';
......@@ -57,7 +56,6 @@ export default function FotoPage() {
const formData = new FormData();
formData.append('picture', blob);
// formData.append('picture', imageSrc); // base64 string
formData.append('latitude_longitude', dataAbsen.latitude_longitude);
formData.append('presence_type', dataAbsen.presence_type);
formData.append('presence_condition', dataAbsen.presence_condition);
......@@ -67,6 +65,7 @@ export default function FotoPage() {
formData.append('office_id', dataAbsen.office_id);
formData.append('office_name', dataAbsen.office_name);
formData.append('location_name', dataAbsen.location_name);
dataAbsen?.parent_id === undefined ? '' :formData.append('parent_id', dataAbsen?.parent_id)
await axiosInstance.post('/user/presence', formData, {
headers: {
......@@ -104,8 +103,8 @@ export default function FotoPage() {
)}
<div className='mt-4'>
{/* <CameraComponent onCapture={(img) => setImageSrc(img)}/> */}
<CameraFaceComponent onCapture={(img) => setImageSrc(img)}/>
{/* <CameraEyeDetection onCapture={(img) => setImageSrc(img)}/> */}
</div>
{imageSrc && (
......
......@@ -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';
......
'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 (
<div className="justify-center p-4">
<div className="pt-[155px]">
<Form
initialValues={{ remember: true }}
onFinish={handleSubmit}
autoComplete="off"
>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
<div>
{contextHolder}
<div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full p-6">
<Image preview={false} src='/assets/logo-login-new.svg' />
<Form
initialValues={{ remember: true }}
onFinish={handleSubmit}
autoComplete="off"
>
<Input placeholder="Email" />
</Form.Item>
<Form.Item
name="username"
rules={[{ required: true, message: 'Mohon masukan username anda!' }]}
>
<Input placeholder="Username" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password placeholder="Password" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Mohon masukan password anda!' }]}
>
<Input.Password placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
Submit
</Button>
</Form.Item>
</Form>
<Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
Submit
</Button>
</Form.Item>
<Button type="primary" block disabled>
Login Azure
</Button>
</Form>
</div>
</div>
</div>
);
......
......@@ -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 (
<div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-gray-200 shadow-md">
<div className="flex justify-around items-center h-16">
{menus.map((menu) => {
const isActive = pathname === menu.path;
return (
<button
key={menu.path}
onClick={() => router.push(menu.path)}
className={classNames(
'flex flex-col items-center justify-center text-sm',
isActive ? 'text-blue-600 font-semibold' : 'text-gray-500'
)}
>
{menu.icon(isActive)}
<span className="text-xs mt-1">{menu.label}</span>
{isActive && <div className="w-1.5 h-1.5 bg-blue-600 rounded-full mt-1" />}
</button>
);
})}
<>
{/* Loading Overlay */}
{loading && (
<div className="fixed inset-0 z-[9999] bg-white bg-opacity-80 flex items-center justify-center">
<Spin size="large" />
</div>
)}
{/* Bottom Menu */}
<div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-gray-200 shadow-md">
<div className="flex justify-around items-center h-16">
{menus.map((menu) => {
const isActive = pathname === menu.path;
return (
<button
key={menu.path}
onClick={() => handleNavigation(menu.path)}
className={classNames(
'flex flex-col items-center justify-center text-sm',
isActive ? 'text-blue-600 font-semibold' : 'text-gray-500'
)}
>
{menu.icon(isActive)}
<span className="text-xs mt-1">{menu.label}</span>
{isActive && <div className="w-1.5 h-1.5 bg-blue-600 rounded-full mt-1" />}
</button>
);
})}
</div>
</div>
</div>
</>
);
}
'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 (
<div>
<div className="relative w-full">
{contextHolder}
{!imageSrc ? (
<>
<Webcam
ref={webcamRef}
audio={false}
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{ facingMode: 'user' }}
style={{ transform: 'scaleX(-1)' }}
/>
<Button type="primary" block onClick={capture} className="mt-4" loading={loading}>
<div className="relative w-full">
<Webcam
ref={webcamRef}
audio={false}
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 }
}}
// style={{ transform: 'scaleX(-1)' }}
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 z-10"
style={{
transform: 'scaleX(-1)',
width: '100%',
height: '100%',
}}
/>
</div>
<div className="text-center mt-2 text-sm">
{faceDetected ? (
<span className="text-green-600">Wajah terdeteksi </span>
) : (
<span className="text-red-500">Arahkan wajah ke kamera </span>
)}
</div>
<Button
type="primary"
block
className="mt-4"
onClick={capture}
disabled={!faceDetected}
>
Ambil Foto
</Button>
</>
......@@ -84,10 +129,10 @@ export default function CameraComponent({ onCapture }) {
<>
<img
src={imageSrc}
alt="Hasil Foto"
className="w-64 h-64 object-cover border rounded mx-auto"
alt="Foto"
className="w-64 h-64 object-cover mx-auto border rounded"
/>
<Button type="primary" block className="mt-4" onClick={reset}>
<Button block className="mt-4" onClick={reset}>
Ulangi Foto
</Button>
</>
......
'use client';
import { useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam';
import { Button, message } from 'antd';
import { loadFaceApi } from '@/utils/loadFaceApi';
export default function CameraFaceComponent({ onCapture }) {
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const [eyesDetected, setEyesDetected] = useState(false);
const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const intervalRef = useRef(null);
useEffect(() => {
initFaceDetection();
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
const initFaceDetection = async () => {
const faceapi = await loadFaceApi();
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');
const displaySize = {
width: video.videoWidth,
height: video.videoHeight,
};
canvas.width = displaySize.width;
canvas.height = displaySize.height;
const detections = await faceapi
.detectAllFaces(
video,
new faceapi.TinyFaceDetectorOptions({
inputSize: 416,
scoreThreshold: 0.5,
})
)
.withFaceLandmarks();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
faceapi.draw.drawDetections(canvas, faceapi.resizeResults(detections, displaySize));
// faceapi.draw.drawFaceLandmarks(canvas, faceapi.resizeResults(detections, displaySize));
ctx.restore();
let isEyesVisible = false;
if (detections.length > 0) {
const landmarks = detections[0].landmarks;
const leftEye = landmarks.getLeftEye();
const rightEye = landmarks.getRightEye();
// ✅ Satu mata pun cukup
isEyesVisible = leftEye.length > 0 || rightEye.length > 0;
}
setEyesDetected(isEyesVisible);
} else {
setEyesDetected(false);
}
}, 400);
};
const capture = () => {
const screenshot = webcamRef.current.getScreenshot();
setImageSrc(screenshot);
onCapture(screenshot);
};
const reset = () => {
setImageSrc(null);
onCapture(null);
};
return (
<div className="relative w-full">
{contextHolder}
{!imageSrc ? (
<>
<div className="relative w-full">
<Webcam
ref={webcamRef}
audio={false}
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 },
}}
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 z-10"
style={{
transform: 'scaleX(-1)',
width: '100%',
height: '100%',
}}
/>
</div>
<div className="text-center mt-2 text-sm">
{eyesDetected ? (
<span className="text-green-600">Wajah terdeteksi </span>
) : (
<span className="text-red-500">Arahkan wajah agar terlihat </span>
)}
</div>
<Button
type="primary"
block
className="mt-4"
onClick={capture}
disabled={!eyesDetected}
>
Ambil Foto
</Button>
</>
) : (
<>
<img
src={imageSrc}
alt="Foto"
className="w-64 h-64 object-cover mx-auto border rounded"
/>
<Button block className="mt-4" onClick={reset}>
Ulangi Foto
</Button>
</>
)}
</div>
);
}
......@@ -8,7 +8,8 @@ import { loadFaceApi } from '@/utils/loadFaceApi';
export default function CameraFaceComponent({ onCapture }) {
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const [faceDetected, setFaceDetected] = useState(false);
const [eyesDetected, setEyesDetected] = useState(false);
const [faceBox, setFaceBox] = useState(null);
const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const intervalRef = useRef(null);
......@@ -38,38 +39,72 @@ export default function CameraFaceComponent({ onCapture }) {
height: video.videoHeight,
};
// Samakan ukuran canvas dengan video
canvas.width = displaySize.width;
canvas.height = displaySize.height;
const detections = await faceapi.detectAllFaces(
video,
new faceapi.TinyFaceDetectorOptions({
inputSize: 416,
scoreThreshold: 0.5,
})
);
const detections = await faceapi
.detectAllFaces(
video,
new faceapi.TinyFaceDetectorOptions({
inputSize: 416,
scoreThreshold: 0.5,
})
)
.withFaceLandmarks();
setFaceDetected(detections.length > 0);
const resized = faceapi.resizeResults(detections, displaySize);
// 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);
faceapi.draw.drawDetections(canvas, faceapi.resizeResults(detections, displaySize));
ctx.restore();
let isEyesVisible = false;
let boundingBox = null;
if (detections.length > 0) {
const detection = detections[0];
const landmarks = detection.landmarks;
const leftEye = landmarks.getLeftEye();
const rightEye = landmarks.getRightEye();
isEyesVisible = leftEye.length > 0 || rightEye.length > 0;
boundingBox = detection.detection.box;
}
setEyesDetected(isEyesVisible);
setFaceBox(boundingBox);
} else {
setEyesDetected(false);
setFaceBox(null);
}
}, 400);
};
const capture = () => {
const screenshot = webcamRef.current.getScreenshot();
setImageSrc(screenshot);
onCapture(screenshot);
};
const video = webcamRef.current.video;
if (!eyesDetected) {
messageApi.error('Wajah tidak terdeteksi!');
return;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
canvas.width = videoWidth;
canvas.height = videoHeight;
ctx.drawImage(video, 0, 0, videoWidth, videoHeight);
const fullImage = canvas.toDataURL('image/jpeg');
setImageSrc(fullImage);
onCapture(fullImage);
};
const reset = () => {
setImageSrc(null);
......@@ -89,29 +124,27 @@ export default function CameraFaceComponent({ onCapture }) {
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 }
}}
// style={{ transform: 'scaleX(-1)' }}
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 },
}}
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 z-10"
style={{
transform: 'scaleX(-1)',
width: '100%',
height: '100%',
}}
ref={canvasRef}
className="absolute top-0 left-0 z-10"
style={{
transform: 'scaleX(-1)',
width: '100%',
height: '100%',
}}
/>
</div>
<div className="text-center mt-2 text-sm">
{faceDetected ? (
{eyesDetected ? (
<span className="text-green-600">Wajah terdeteksi </span>
) : (
<span className="text-red-500">Arahkan wajah ke kamera </span>
<span className="text-red-500">Arahkan wajah agar terlihat </span>
)}
</div>
......@@ -120,7 +153,7 @@ export default function CameraFaceComponent({ onCapture }) {
block
className="mt-4"
onClick={capture}
disabled={!faceDetected}
disabled={!eyesDetected || !faceBox}
>
Ambil Foto
</Button>
......
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 (
<div className='mt-4' >
<Badge.Ribbon text={getDayName(date)}
style={checkDayWork(date, presences) === 'Hari Kerja' ?
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' } :
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' }
}
>
<div className='card-shadow cursor-pointer hover:shadow-lg transition'
onClick={handleClick}>
<div className='font-bold'>
{formatTanggalIndo(date)}
</div>
<div className='mt-4 mb-4'>
<Row className="gutter-row" span={12}>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.time || '00:00:00'} WIB</div>
</Flex>
</Col>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB</div>
</Flex>
</Col>
</Row>
<div>
{loading ?
<>
<LoadingComponent />
</> :
<div className='mt-4' >
<Badge.Ribbon text={getDayName(date)}
style={checkDayWork(date, presences) === 'Hari Kerja' ?
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' } :
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' }
}
>
<div className='card-shadow cursor-pointer hover:shadow-lg transition'
onClick={handleClick}>
<div className='font-bold'>
{formatTanggalIndo(date)}
</div>
<div className='mt-4 mb-4'>
<Row className="gutter-row" span={12}>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.time || '00:00:00'} WIB</div>
</Flex>
</Col>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB</div>
</Flex>
{moment().subtract(1, "days").format("DD MMMM YYYY") == moment(presences[0]?.date).format("DD MMMM YYYY") ?
presences[0]?.presence === null ?
hours <= 23 ?
// hours <= 16 ?
<div className='mt-2'>
<Button
className="text-12"
type='primary'
block
size='small'
onClick={(e) => {
e.stopPropagation()
handelCheckOutDelay(presences)
}}
loading={loadingCheckOutTelat}
>
<div className='text-sm mt-1'>
Pulang
</div>
</Button>
</div>
: ""
: ""
: ""
}
</Col>
</Row>
</div>
</div>
</div>
</Badge.Ribbon>
</Badge.Ribbon>
</div>
}
</div>
);
}
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 (
<div className='mt-4'>
<div className='card bg-white'>
<div>
<Flex justify='flex-start' align='center' gap={'small'}>
<div className='max-w-sm'>
{!presenceUrl ?
presenceType === 'Datang' ?
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
:
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
:
<Avatar shape='square' size="middle" src="/bg/prof.jpg" />
}
</div>
<div className='text-sm'>
{presenceType === 'Datang' ?
<div className='font-bold text-sm'>{presenceData?.time || '00:00'} WIB</div>
:
<div className='font-bold text-sm'>{presenceData?.presence?.time || '00:00'} WIB</div>
}
</div>
</Flex>
<div className='mt-2'>
{presenceType === "Datang"
?
(
<Button size='small' type='primary' onClick={() => handleClick(presenceType)} block disabled={presenceData?.presence_type === 'masuk'}>
<div className='text-sm'>
Datang
</div>
</Button>
) :
(
<Button size='small' type='primary' onClick={() => handleClick(presenceType)} block disabled={presenceData?.presence?.presence_type === 'pulang' || !presenceData ? true : false}>
<div className='text-sm'>
Pulang
</div>
</Button>
)
}
</div>
<div className='card bg-white'>
<div>
<Flex justify='flex-start' align='center' gap={'small'}>
<div className='max-w-sm'>
{!presenceUrl ? (
presenceType === 'Datang' ? (
<Avatar size="middle" src={<LogIn size={16} style={{ color: "#443627" }} />} style={{ background: '#EFDCAB' }} />
) : (
<Avatar size="middle" src={<LogOut size={16} style={{ color: "#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB' }} />
)
) : (
<Avatar shape='square' size="middle" src="/bg/prof.jpg" />
)}
</div>
<div className='text-sm font-bold'>
{presenceType === 'Datang'
? `${presenceData?.time || '00:00'} WIB`
: `${presenceData?.presence?.time || '00:00'} WIB`}
</div>
</Flex>
<div className='mt-2'>
<Button
size='small'
type='primary'
block
loading={loading}
onClick={onClick}
disabled={
presenceType === 'Datang'
? presenceData?.presence_type === 'masuk'
: presenceData?.presence?.presence_type === 'pulang' || !presenceData
}
>
<div className='text-sm'>{presenceType}</div>
</Button>
</div>
</div>
</div>
</div>
);
}
......@@ -13,7 +13,7 @@ export default function ProfileCardComponent({ name, image }) {
<div className='flex justify-between'>
<div>
<div className='font-bold '>{session?.user?.name || '-'}</div>
<div>{session?.user?.nip || 'NIP'}</div>
<div>{session?.nip || 'NIP'}</div>
</div>
<Avatar style={{ border: '2px solid #D98324' }} size={'large'} src={"/bg/prof.jpg"} />
</div>
......
......@@ -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();
}
......
......@@ -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;
};
......@@ -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
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