Commit 5f9b580a authored by jorke's avatar jorke

Initial commit

parent fef8775c
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"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",
"moment": "^2.30.1",
"next": "15.3.3", "next": "15.3.3",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
...@@ -5593,6 +5594,15 @@ ...@@ -5593,6 +5594,15 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"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",
"moment": "^2.30.1",
"next": "15.3.3", "next": "15.3.3",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
......
<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() { ...@@ -21,6 +21,7 @@ export default function Beranda() {
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');
const [loadingPresenceType, setLoadingPresenceType] = useState(null); // 'Datang' atau 'Pulang'
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
...@@ -42,17 +43,25 @@ export default function Beranda() { ...@@ -42,17 +43,25 @@ export default function Beranda() {
const fetchedData = response?.data ?? []; const fetchedData = response?.data ?? [];
const absenData = fetchedData.data[0]; const absenData = fetchedData.data[0];
setDataAbsenToday(fetchedData.data[0]); setDataAbsenToday(fetchedData.data[0]);
console.log(absenData);
// console.log(absenData, 'dataAbsen');
if (absenData?.presence_type === 'masuk') { if (absenData?.presence_type === 'masuk') {
document.cookie = 'hasCheckedIn=true; path=/;'; document.cookie = 'hasCheckedIn=true; path=/;';
} else { } 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') { if (absenData?.presence?.presence_type === 'pulang') {
document.cookie = 'hasCheckedOut=true; path=/;'; document.cookie = 'hasCheckedOut=true; path=/;';
} else { } 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) { } catch (error) {
...@@ -85,6 +94,12 @@ export default function Beranda() { ...@@ -85,6 +94,12 @@ export default function Beranda() {
setLoadingAbsenHistory(false); setLoadingAbsenHistory(false);
} }
const handlePresenceClick = (type) => {
setLoadingPresenceType(type);
const targetRoute = type === 'Datang' ? '/check-in' : '/check-out';
router.push(targetRoute);
};
return ( return (
...@@ -109,8 +124,23 @@ export default function Beranda() { ...@@ -109,8 +124,23 @@ export default function Beranda() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4 "> <div className="grid grid-cols-2 gap-4 ">
<PresenceCard presenceType={typePresenceIn} presenceData={dataAbsenToday}/> {/* <PresenceCard presenceType={typePresenceIn} presenceData={dataAbsenToday}/>
<PresenceCard presenceType={typePresenceOut} 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> </div>
<div className="grid gap-4 mt-4"> <div className="grid gap-4 mt-4">
......
This diff is collapsed.
...@@ -222,7 +222,7 @@ export default function CheckIn() { ...@@ -222,7 +222,7 @@ export default function CheckIn() {
{contextHolder} {contextHolder}
<div className="p-4 card-head"> <div className="p-4 card-head">
<div className="grid gap-4 mt-4"> <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 className="col-end-3 text-end text-blue-600">
<div> <div>
<Button <Button
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Button, message } from 'antd'; import { Button, message } from 'antd';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import CameraComponent from '@/components/CameraComponent';
import axiosInstance from '@/lib/axios'; import axiosInstance from '@/lib/axios';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import CameraFaceComponent from '@/components/CameraFaceComponent'; import CameraFaceComponent from '@/components/CameraFaceComponent';
...@@ -57,7 +56,6 @@ export default function FotoPage() { ...@@ -57,7 +56,6 @@ export default function FotoPage() {
const formData = new FormData(); const formData = new FormData();
formData.append('picture', blob); formData.append('picture', blob);
// formData.append('picture', imageSrc); // base64 string
formData.append('latitude_longitude', dataAbsen.latitude_longitude); formData.append('latitude_longitude', dataAbsen.latitude_longitude);
formData.append('presence_type', dataAbsen.presence_type); formData.append('presence_type', dataAbsen.presence_type);
formData.append('presence_condition', dataAbsen.presence_condition); formData.append('presence_condition', dataAbsen.presence_condition);
...@@ -67,6 +65,7 @@ export default function FotoPage() { ...@@ -67,6 +65,7 @@ export default function FotoPage() {
formData.append('office_id', dataAbsen.office_id); formData.append('office_id', dataAbsen.office_id);
formData.append('office_name', dataAbsen.office_name); formData.append('office_name', dataAbsen.office_name);
formData.append('location_name', dataAbsen.location_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, { await axiosInstance.post('/user/presence', formData, {
headers: { headers: {
...@@ -104,8 +103,8 @@ export default function FotoPage() { ...@@ -104,8 +103,8 @@ export default function FotoPage() {
)} )}
<div className='mt-4'> <div className='mt-4'>
{/* <CameraComponent onCapture={(img) => setImageSrc(img)}/> */}
<CameraFaceComponent onCapture={(img) => setImageSrc(img)}/> <CameraFaceComponent onCapture={(img) => setImageSrc(img)}/>
{/* <CameraEyeDetection onCapture={(img) => setImageSrc(img)}/> */}
</div> </div>
{imageSrc && ( {imageSrc && (
......
...@@ -2,7 +2,7 @@ import '../../styles/globals.css'; ...@@ -2,7 +2,7 @@ import '../../styles/globals.css';
import { AntdRegistry } from '@ant-design/nextjs-registry'; import { AntdRegistry } from '@ant-design/nextjs-registry';
import React from 'react'; import React from 'react';
import { DM_Sans } from 'next/font/google' import { DM_Sans } from 'next/font/google'
import { ConfigProvider } from 'antd'; import { ConfigProvider, App } from 'antd';
import theme from '../../theme.config'; import theme from '../../theme.config';
import '@ant-design/v5-patch-for-react-19'; import '@ant-design/v5-patch-for-react-19';
import ConditionalBottomMenu from '@/components/ConditionalBottomMenu'; import ConditionalBottomMenu from '@/components/ConditionalBottomMenu';
......
'use client' 'use client'
import React, { useEffect, useState } from 'react'; 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 { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
...@@ -11,6 +11,7 @@ export default function Login() { ...@@ -11,6 +11,7 @@ export default function Login() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
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();
useEffect(() => { useEffect(() => {
if (error) { if (error) {
...@@ -27,41 +28,49 @@ export default function Login() { ...@@ -27,41 +28,49 @@ export default function Login() {
}); });
if (result.ok) { if (result.ok) {
messageApi.success('Login Berhasil')
router.push("/beranda"); router.push("/beranda");
} else { } else {
alert("Login gagal"); messageApi.error("Login Gagal, Cek kembali username dan password");
} }
setLoading(false); setLoading(false);
}; };
return ( return (
<div className="justify-center p-4"> <div>
<div className="pt-[155px]"> {contextHolder}
<Form <div className="min-h-screen flex items-center justify-center px-4">
initialValues={{ remember: true }} <div className="max-w-md w-full p-6">
onFinish={handleSubmit} <Image preview={false} src='/assets/logo-login-new.svg' />
autoComplete="off" <Form
> initialValues={{ remember: true }}
<Form.Item onFinish={handleSubmit}
name="username" autoComplete="off"
rules={[{ required: true, message: 'Please input your username!' }]}
> >
<Input placeholder="Email" /> <Form.Item
</Form.Item> name="username"
rules={[{ required: true, message: 'Mohon masukan username anda!' }]}
>
<Input placeholder="Username" />
</Form.Item>
<Form.Item <Form.Item
name="password" name="password"
rules={[{ required: true, message: 'Please input your password!' }]} rules={[{ required: true, message: 'Mohon masukan password anda!' }]}
> >
<Input.Password placeholder="Password" /> <Input.Password placeholder="Password" />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}> <Button type="primary" htmlType="submit" block loading={loading}>
Submit Submit
</Button> </Button>
</Form.Item> </Form.Item>
</Form> <Button type="primary" block disabled>
Login Azure
</Button>
</Form>
</div>
</div> </div>
</div> </div>
); );
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { CalendarCheck, Home, User } from 'lucide-react'; import { CalendarCheck, Home, User } from 'lucide-react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { Spin } from 'antd';
export default function BottomMenuComponent() { export default function BottomMenuComponent() {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false);
const menus = [ const menus = [
{ {
...@@ -26,28 +29,49 @@ export default function BottomMenuComponent() { ...@@ -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 ( 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"> {/* Loading Overlay */}
{menus.map((menu) => { {loading && (
const isActive = pathname === menu.path; <div className="fixed inset-0 z-[9999] bg-white bg-opacity-80 flex items-center justify-center">
<Spin size="large" />
return ( </div>
<button )}
key={menu.path}
onClick={() => router.push(menu.path)} {/* Bottom Menu */}
className={classNames( <div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-gray-200 shadow-md">
'flex flex-col items-center justify-center text-sm', <div className="flex justify-around items-center h-16">
isActive ? 'text-blue-600 font-semibold' : 'text-gray-500' {menus.map((menu) => {
)} const isActive = pathname === menu.path;
>
{menu.icon(isActive)} return (
<span className="text-xs mt-1">{menu.label}</span> <button
{isActive && <div className="w-1.5 h-1.5 bg-blue-600 rounded-full mt-1" />} key={menu.path}
</button> 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>
</div> </>
); );
} }
'use client'; 'use client';
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam'; import Webcam from 'react-webcam';
import { Button, message } from 'antd'; import { Button, message } from 'antd';
import { loadFaceApi } from '@/utils/loadFaceApi';
// Fungsi kompresi gambar agar < 500KB export default function CameraComponent({ onCapture }) {
export const compressImage = async (base64, maxSizeKB = 500, quality = 0.7) => { const webcamRef = useRef(null);
return new Promise((resolve) => { const canvasRef = useRef(null);
const img = new Image(); const [faceDetected, setFaceDetected] = useState(false);
img.src = base64; const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const intervalRef = useRef(null);
img.onload = () => { useEffect(() => {
const canvas = document.createElement('canvas'); initFaceDetection();
const ctx = canvas.getContext('2d'); return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
canvas.width = img.width; const initFaceDetection = async () => {
canvas.height = img.height; const faceapi = await loadFaceApi();
// Flip secara horizontal (mirror) intervalRef.current = setInterval(async () => {
ctx.translate(canvas.width, 0); if (
ctx.scale(-1, 1); webcamRef.current &&
ctx.drawImage(img, 0, 0); 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); const displaySize = {
let sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024); width: video.videoWidth,
height: video.videoHeight,
};
// Loop jika masih > maxSizeKB // Samakan ukuran canvas dengan video
while (sizeInKB > maxSizeKB && quality > 0.1) { canvas.width = displaySize.width;
quality -= 0.05; canvas.height = displaySize.height;
compressedBase64 = canvas.toDataURL('image/jpeg', quality);
sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024);
}
resolve(compressedBase64); const detections = await faceapi.detectAllFaces(
}; video,
}); new faceapi.TinyFaceDetectorOptions({
}; inputSize: 416,
scoreThreshold: 0.5,
})
);
export default function CameraComponent({ onCapture }) { setFaceDetected(detections.length > 0);
const webcamRef = useRef(null);
const [imageSrc, setImageSrc] = useState(null); const resized = faceapi.resizeResults(detections, displaySize);
const [loading, setLoading] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const capture = async () => { // Gambar bounding box mirror
if (!webcamRef.current) return; 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(); const screenshot = webcamRef.current.getScreenshot();
if (!screenshot) { setImageSrc(screenshot);
messageApi.error('Gagal mengambil gambar'); onCapture(screenshot);
return;
}
setLoading(true);
const compressed = await compressImage(screenshot);
setImageSrc(compressed);
setLoading(false);
onCapture(compressed);
}; };
const reset = () => { const reset = () => {
...@@ -65,18 +77,51 @@ export default function CameraComponent({ onCapture }) { ...@@ -65,18 +77,51 @@ export default function CameraComponent({ onCapture }) {
}; };
return ( return (
<div> <div className="relative w-full">
{contextHolder}
{!imageSrc ? ( {!imageSrc ? (
<> <>
<Webcam <div className="relative w-full">
ref={webcamRef} <Webcam
audio={false} ref={webcamRef}
screenshotFormat="image/jpeg" audio={false}
width="100%" screenshotFormat="image/jpeg"
videoConstraints={{ facingMode: 'user' }} width="100%"
style={{ transform: 'scaleX(-1)' }} videoConstraints={{
/> facingMode: 'user',
<Button type="primary" block onClick={capture} className="mt-4" loading={loading}> 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 Ambil Foto
</Button> </Button>
</> </>
...@@ -84,10 +129,10 @@ export default function CameraComponent({ onCapture }) { ...@@ -84,10 +129,10 @@ export default function CameraComponent({ onCapture }) {
<> <>
<img <img
src={imageSrc} src={imageSrc}
alt="Hasil Foto" alt="Foto"
className="w-64 h-64 object-cover border rounded mx-auto" 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 Ulangi Foto
</Button> </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'; ...@@ -8,7 +8,8 @@ import { loadFaceApi } from '@/utils/loadFaceApi';
export default function CameraFaceComponent({ onCapture }) { export default function CameraFaceComponent({ onCapture }) {
const webcamRef = useRef(null); const webcamRef = useRef(null);
const canvasRef = 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 [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const intervalRef = useRef(null); const intervalRef = useRef(null);
...@@ -38,38 +39,72 @@ export default function CameraFaceComponent({ onCapture }) { ...@@ -38,38 +39,72 @@ export default function CameraFaceComponent({ onCapture }) {
height: video.videoHeight, height: video.videoHeight,
}; };
// Samakan ukuran canvas dengan video
canvas.width = displaySize.width; canvas.width = displaySize.width;
canvas.height = displaySize.height; canvas.height = displaySize.height;
const detections = await faceapi.detectAllFaces( const detections = await faceapi
video, .detectAllFaces(
new faceapi.TinyFaceDetectorOptions({ video,
inputSize: 416, new faceapi.TinyFaceDetectorOptions({
scoreThreshold: 0.5, 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.clearRect(0, 0, canvas.width, canvas.height);
ctx.save(); ctx.save();
ctx.scale(-1, 1); ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0); ctx.translate(-canvas.width, 0);
faceapi.draw.drawDetections(canvas, resized); faceapi.draw.drawDetections(canvas, faceapi.resizeResults(detections, displaySize));
ctx.restore(); 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); }, 400);
}; };
const capture = () => { const capture = () => {
const screenshot = webcamRef.current.getScreenshot(); const video = webcamRef.current.video;
setImageSrc(screenshot);
onCapture(screenshot); 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 = () => { const reset = () => {
setImageSrc(null); setImageSrc(null);
...@@ -89,29 +124,27 @@ export default function CameraFaceComponent({ onCapture }) { ...@@ -89,29 +124,27 @@ export default function CameraFaceComponent({ onCapture }) {
screenshotFormat="image/jpeg" screenshotFormat="image/jpeg"
width="100%" width="100%"
videoConstraints={{ videoConstraints={{
facingMode: 'user', facingMode: 'user',
width: { ideal: 640 }, width: { ideal: 640 },
height: { ideal: 480 } height: { ideal: 480 },
}} }}
// style={{ transform: 'scaleX(-1)' }}
/> />
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="absolute top-0 left-0 z-10" className="absolute top-0 left-0 z-10"
style={{ style={{
transform: 'scaleX(-1)', transform: 'scaleX(-1)',
width: '100%', width: '100%',
height: '100%', height: '100%',
}} }}
/> />
</div> </div>
<div className="text-center mt-2 text-sm"> <div className="text-center mt-2 text-sm">
{faceDetected ? ( {eyesDetected ? (
<span className="text-green-600">Wajah terdeteksi </span> <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> </div>
...@@ -120,7 +153,7 @@ export default function CameraFaceComponent({ onCapture }) { ...@@ -120,7 +153,7 @@ export default function CameraFaceComponent({ onCapture }) {
block block
className="mt-4" className="mt-4"
onClick={capture} onClick={capture}
disabled={!faceDetected} disabled={!eyesDetected || !faceBox}
> >
Ambil Foto Ambil Foto
</Button> </Button>
......
import React from 'react'; import React, { useState } from 'react';
import { Card, Avatar, Row, Col, Flex, Badge, Button } from 'antd'; import { Card, Avatar, Row, Col, Flex, Badge, Button } from 'antd';
import { CloseCircleOutlined, LoginOutlined, LogoutOutlined } from '@ant-design/icons'; import { CloseCircleOutlined, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { LogIn, LogOut } from 'lucide-react'; import { LogIn, LogOut } from 'lucide-react';
import {useRouter} from 'next/navigation'; import {useRouter} from 'next/navigation';
import usePresenceStore from '@/stores/usePresenceStore'; import usePresenceStore from '@/stores/usePresenceStore';
import LoadingComponent from './LoadingComponent';
import moment from "moment";
const { Meta } = Card; const { Meta } = Card;
export default function DetailPresenceCardComponent({ date, presences }) { export default function DetailPresenceCardComponent({ date, presences }) {
const router = useRouter(); const router = useRouter();
const setPresenceData = usePresenceStore((state) => state.setPresenceData); const setPresenceData = usePresenceStore((state) => state.setPresenceData);
const [loading, setLoading] = useState(false);
const [loadingCheckOutTelat, setLoadingCheckOutTelat] = useState(false);
function getDayName(dateString) { function getDayName(dateString) {
...@@ -27,11 +31,12 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -27,11 +31,12 @@ export default function DetailPresenceCardComponent({ date, presences }) {
} }
const handleClick = () => { const handleClick = () => {
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) => {
if (getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu'){ if (getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu'){
if (!presences[0]) { if (!presences[0]) {
...@@ -44,38 +49,86 @@ export default function DetailPresenceCardComponent({ date, presences }) { ...@@ -44,38 +49,86 @@ export default function DetailPresenceCardComponent({ date, presences }) {
} }
} }
// getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu' ? !presences[0] ? 'Hari Libur' : 'Hari Kerja' : 'Hari Kerja' // 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 ( return (
<div className='mt-4' > <div>
<Badge.Ribbon text={getDayName(date)} {loading ?
style={checkDayWork(date, presences) === 'Hari Kerja' ? <>
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' } : <LoadingComponent />
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' } </> :
} <div className='mt-4' >
> <Badge.Ribbon text={getDayName(date)}
<div className='card-shadow cursor-pointer hover:shadow-lg transition' style={checkDayWork(date, presences) === 'Hari Kerja' ?
onClick={handleClick}> { width: 80, textAlign: 'center', backgroundColor:'#D98324' } :
<div className='font-bold'> { width: 80, textAlign: 'center', backgroundColor:'#D98324' }
{formatTanggalIndo(date)} }
</div> >
<div className='mt-4 mb-4'> <div className='card-shadow cursor-pointer hover:shadow-lg transition'
<Row className="gutter-row" span={12}> onClick={handleClick}>
<Col span={12}> <div className='font-bold'>
<Flex align='center' gap={'small'}> {formatTanggalIndo(date)}
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} /> </div>
<div className='text-md'>{presences[0]?.time || '00:00:00'} WIB</div> <div className='mt-4 mb-4'>
</Flex> <Row className="gutter-row" span={12}>
</Col> <Col span={12}>
<Col span={12}> <Flex align='center' gap={'small'}>
<Flex align='center' gap={'small'}> <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'}} /> <div className='text-md'>{presences[0]?.time || '00:00:00'} WIB</div>
<div className='text-md'>{presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB</div> </Flex>
</Flex> </Col>
</Col> <Col span={12}>
</Row> <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>
</div> </Badge.Ribbon>
</Badge.Ribbon> </div>
}
</div> </div>
); );
} }
import React, { useEffect, useState } from 'react'; import { Avatar, Button, Flex } from 'antd';
import { Card, Avatar, Row, Col, Flex, Badge, Button, Tag } from 'antd';
import Icon, { CloseCircleOutlined, CloseCircleTwoTone, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { LogIn, LogOut } from 'lucide-react'; 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 ( return (
<div className='mt-4'> <div className='mt-4'>
<div className='card bg-white'> <div className='card bg-white'>
<div> <div>
<Flex justify='flex-start' align='center' gap={'small'}> <Flex justify='flex-start' align='center' gap={'small'}>
<div className='max-w-sm'> <div className='max-w-sm'>
{!presenceUrl ? {!presenceUrl ? (
presenceType === 'Datang' ? presenceType === 'Datang' ? (
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} /> <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 size="middle" src={<LogOut size={16} style={{ color: "#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB' }} />
: )
<Avatar shape='square' size="middle" src="/bg/prof.jpg" /> ) : (
} <Avatar shape='square' size="middle" src="/bg/prof.jpg" />
</div> )}
</div>
<div className='text-sm'> <div className='text-sm font-bold'>
{presenceType === 'Datang' ? {presenceType === 'Datang'
<div className='font-bold text-sm'>{presenceData?.time || '00:00'} WIB</div> ? `${presenceData?.time || '00:00'} WIB`
: : `${presenceData?.presence?.time || '00:00'} WIB`}
<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> </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>
</div> </div>
); );
} }
...@@ -13,7 +13,7 @@ export default function ProfileCardComponent({ name, image }) { ...@@ -13,7 +13,7 @@ export default function ProfileCardComponent({ name, image }) {
<div className='flex justify-between'> <div className='flex justify-between'>
<div> <div>
<div className='font-bold '>{session?.user?.name || '-'}</div> <div className='font-bold '>{session?.user?.name || '-'}</div>
<div>{session?.user?.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={"/bg/prof.jpg"} />
</div> </div>
......
...@@ -37,6 +37,13 @@ export async function middleware(req) { ...@@ -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(); return NextResponse.next();
} }
......
...@@ -5,7 +5,10 @@ export const loadFaceApi = async () => { ...@@ -5,7 +5,10 @@ export const loadFaceApi = async () => {
const mod = await import('@vladmandic/face-api'); const mod = await import('@vladmandic/face-api');
faceapi = mod; faceapi = mod;
await faceapi.nets.tinyFaceDetector.loadFromUri('/models'); await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri('/models'),
faceapi.nets.faceLandmark68Net.loadFromUri('/models'),
]);
} }
return faceapi; return faceapi;
}; };
...@@ -10,12 +10,12 @@ export const getLocationAddress = async (lat, lon) => { ...@@ -10,12 +10,12 @@ export const getLocationAddress = async (lat, lon) => {
} else { } else {
// Jika error dari server (misal rate limit, invalid key, dll) // Jika error dari server (misal rate limit, invalid key, dll)
const errorData = await response.json(); const errorData = await response.json();
console.error('Error fetching location:', errorData); // console.error('Error fetching location:', errorData);
return 'Alamat tidak terdeteksi'; return 'Alamat tidak terdeteksi';
} }
} catch (error) { } catch (error) {
// Jika error jaringan atau timeout // Jika error jaringan atau timeout
console.error('Network error:', error); // console.error('Network error:', error);
return 'Alamat tidak terdeteksi'; 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