From 33f3c8df90e435ea9b2170d49102bb6afbf871c1 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 11:47:13 +0700 Subject: [PATCH 01/28] fix: use window.location.origin for scan URL instead of hardcoded localhost --- lentera-frontend/app/display/page.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lentera-frontend/app/display/page.tsx b/lentera-frontend/app/display/page.tsx index 3d9b113..9469e7f 100644 --- a/lentera-frontend/app/display/page.tsx +++ b/lentera-frontend/app/display/page.tsx @@ -34,8 +34,11 @@ export default function DisplayQR() { return () => clearInterval(interval); }, []); - const frontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || (typeof window !== 'undefined' ? window.location.origin : ''); - const scanUrl = frontendUrl ? `${frontendUrl}/scan?token=${qrToken}` : ''; + const [scanUrl, setScanUrl] = useState(''); + + useEffect(() => { + setScanUrl(`${window.location.origin}/scan?token=${qrToken}`); + }, [qrToken]); if (error) { return ( From 32725135de200d8c399a53c841814bc2d6cb6940 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 11:50:22 +0700 Subject: [PATCH 02/28] chore: remove Dev Mode text from display page --- lentera-frontend/app/display/page.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/lentera-frontend/app/display/page.tsx b/lentera-frontend/app/display/page.tsx index 9469e7f..e248455 100644 --- a/lentera-frontend/app/display/page.tsx +++ b/lentera-frontend/app/display/page.tsx @@ -34,12 +34,6 @@ export default function DisplayQR() { return () => clearInterval(interval); }, []); - const [scanUrl, setScanUrl] = useState(''); - - useEffect(() => { - setScanUrl(`${window.location.origin}/scan?token=${qrToken}`); - }, [qrToken]); - if (error) { return (
@@ -74,7 +68,7 @@ export default function DisplayQR() {
{qrToken ? ( )} - -
-

Dev Mode: Klik link di bawah untuk test tanpa scan:

- - {scanUrl} - -
); } From c5bc8e9807093237b506c2c5e0f39ef1cf389372 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:16:39 +0700 Subject: [PATCH 03/28] chore: remove Dockerfile & start.sh, add README with tech stack --- Dockerfile | 21 --------------------- README.md | 18 ++++++++++++++++++ start.sh | 25 ------------------------- 3 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 Dockerfile create mode 100644 README.md delete mode 100755 start.sh diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 22367d5..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM php:8.3-fpm-alpine - -RUN apk add --no-linux-headers --no-cache \ - zip unzip curl libpng-dev libxml2-dev libzip-dev postgresql-dev nginx supervisor - -RUN docker-php-ext-install pdo pdo_pgsql zip bcmath gd - -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -WORKDIR /var/www/html - -COPY lentera-backend/ . - -RUN composer install --no-dev --optimize-autoloader - -RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache -RUN chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache - -EXPOSE 80 - -CMD ["sh", "-c", "php artisan migrate --force && php artisan config:cache && php artisan route:cache && php artisan serve --host=0.0.0.0 --port=80"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..0360cbf --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Lentera - Lending & Tracking Application + +A simple and secure laboratory asset lending and tracking system. This project was developed to fulfill the exam practicum assignment and later adapted for production deployment. + +## Tech Stack + +- **Frontend:** Next.js 16, React 19, TypeScript +- **Styling:** Tailwind CSS v4 +- **Database & Auth:** Supabase (PostgreSQL) +- **API Layer:** Vercel Serverless Functions (Next.js API Routes) +- **Deployment:** Vercel +- **Original Backend:** Laravel +- **Libraries:** Axios, bcryptjs, qrcode.react, @vercel/analytics + +## Developed By + +- **Backend:** [@rannd1nt](https://github.com/rannd1nt) (Zahraan Dzakii Ts.) +- **Frontend:** [@MuadzHdz](https://github.com/MuadzHdz) (Mu'adz Hudzaifah) diff --git a/start.sh b/start.sh deleted file mode 100755 index 0081a2d..0000000 --- a/start.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -e - -cleanup() { - echo "" - echo "Shutting down..." - kill "$BACKEND_PID" 2>/dev/null || true - kill "$FRONTEND_PID" 2>/dev/null || true - exit 0 -} -trap cleanup SIGINT SIGTERM - -cd "$(dirname "$0")" - -(cd lentera-backend && php artisan serve --host=0.0.0.0 --port=8000) & -BACKEND_PID=$! - -(cd lentera-frontend && npm run dev) & -FRONTEND_PID=$! - -echo "Backend : http://localhost:8000" -echo "Frontend : http://localhost:3000" -echo "Press Ctrl+C to stop both services." - -wait From d46a79d3c510f0fd7ad7e288f238e72f33db61d9 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:31:59 +0700 Subject: [PATCH 04/28] chore: add vercel.json with rootDirectory for auto-deploy --- vercel.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..a78980b --- /dev/null +++ b/vercel.json @@ -0,0 +1,4 @@ +{ + "name": "lentera", + "rootDirectory": "lentera-frontend" +} From ce15cc406fb0abf6637c6668c0c1df128aa0e885 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:39:40 +0700 Subject: [PATCH 05/28] test: trigger auto-deploy from production branch --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e985853 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vercel From 6ed6b90684d487c72426bb128c0500a0b977bbd7 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:40:21 +0700 Subject: [PATCH 06/28] chore: remove accidental .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e985853..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vercel From 56e5b48cdc1fcd278ff0e0fca6d6e3343895530f Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:41:39 +0700 Subject: [PATCH 07/28] ci: add GitHub Actions workflow for Vercel deployment --- .github/workflows/deploy.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3180de4 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,18 @@ +name: Deploy to Vercel Production + +on: + push: + branches: [production] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm ci + working-directory: lentera-frontend + - run: npx vercel --prod --yes --token=${{ secrets.VERCEL_TOKEN }} + working-directory: lentera-frontend From abd2584879e62f918b87156cf8505f74a1f54812 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:44:25 +0700 Subject: [PATCH 08/28] chore: remove vercel.json (rootDirectory sudah di-set via API) --- vercel.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 vercel.json diff --git a/vercel.json b/vercel.json deleted file mode 100644 index a78980b..0000000 --- a/vercel.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "lentera", - "rootDirectory": "lentera-frontend" -} From 540d1e3fd7041ec2b5858d56f7671dd3abf6e18f Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:46:32 +0700 Subject: [PATCH 09/28] ci: add vercel link step to workflow --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3180de4..3bdf9b4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,5 +14,7 @@ jobs: node-version: 24 - run: npm ci working-directory: lentera-frontend + - run: npx vercel link --project prj_YpwoVilhpNOGVH3XErGd8uoCyoLL --scope mu-adz-hudzaifah-s-projects --yes + working-directory: lentera-frontend - run: npx vercel --prod --yes --token=${{ secrets.VERCEL_TOKEN }} working-directory: lentera-frontend From af9cd051138de87756dc181c8af8d547f6787bf3 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:48:20 +0700 Subject: [PATCH 10/28] fix: commit .vercel/project.json for GitHub Actions deployment --- .github/workflows/deploy.yml | 2 -- lentera-frontend/.gitignore | 2 +- lentera-frontend/.vercel/project.json | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 lentera-frontend/.vercel/project.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3bdf9b4..3180de4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,5 @@ jobs: node-version: 24 - run: npm ci working-directory: lentera-frontend - - run: npx vercel link --project prj_YpwoVilhpNOGVH3XErGd8uoCyoLL --scope mu-adz-hudzaifah-s-projects --yes - working-directory: lentera-frontend - run: npx vercel --prod --yes --token=${{ secrets.VERCEL_TOKEN }} working-directory: lentera-frontend diff --git a/lentera-frontend/.gitignore b/lentera-frontend/.gitignore index 5ef6a52..b83e760 100644 --- a/lentera-frontend/.gitignore +++ b/lentera-frontend/.gitignore @@ -34,7 +34,7 @@ yarn-error.log* .env* # vercel -.vercel +.vercel/*.env # typescript *.tsbuildinfo diff --git a/lentera-frontend/.vercel/project.json b/lentera-frontend/.vercel/project.json new file mode 100644 index 0000000..178247a --- /dev/null +++ b/lentera-frontend/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_YpwoVilhpNOGVH3XErGd8uoCyoLL","orgId":"team_p7Y24AzkJDCLJMVSGhyG4D5a","projectName":"lentera"} \ No newline at end of file From 218fec80513841b7956c45d3cba3e76346989425 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 12:51:00 +0700 Subject: [PATCH 11/28] ci: add --scope flag to vercel deploy --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3180de4..35f430f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,5 +14,5 @@ jobs: node-version: 24 - run: npm ci working-directory: lentera-frontend - - run: npx vercel --prod --yes --token=${{ secrets.VERCEL_TOKEN }} + - run: npx vercel --prod --yes --scope mu-adz-hudzaifah-s-projects --token=${{ secrets.VERCEL_TOKEN }} working-directory: lentera-frontend From 874cd8e38ab1caf2f053b291a013d85f6dc8dc4a Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:18:33 +0700 Subject: [PATCH 12/28] UX: toast bottom-right, sticky header, konfirmasi modal & loading state --- lentera-frontend/app/admin/dashboard/page.tsx | 47 +++++++++++++++++-- lentera-frontend/components/ui/Toast.tsx | 2 +- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index 473de3e..22bdb44 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -48,6 +48,8 @@ export default function AdminDashboard() { return { isOpen: false, ...defaults, ...parsed }; }); const [qrModal, setQrModal] = useState<{isOpen: boolean, asset: QrModalData}>({isOpen: false, asset: null}); + const [confirmModal, setConfirmModal] = useState<'batch-qr' | 'export-csv' | null>(null); + const [actionLoading, setActionLoading] = useState(false); const qrRef = useRef(null); const fetchData = useCallback(async () => { @@ -260,9 +262,10 @@ export default function AdminDashboard() { // Feature #7: Batch download all QR codes const downloadAllQR = async () => { + setActionLoading(true); toast.info("Menyiapkan download semua QR code..."); const containers = qrBatchRef.current?.querySelectorAll('[data-qr-batch]'); - if (!containers) return; + if (!containers) { setActionLoading(false); return; } for (const container of Array.from(containers)) { const canvas = container.querySelector('canvas'); if (!canvas) continue; @@ -277,6 +280,7 @@ export default function AdminDashboard() { link.click(); await new Promise(resolve => setTimeout(resolve, 300)); } + setActionLoading(false); toast.success("Semua QR code berhasil di-download!"); }; @@ -292,6 +296,7 @@ export default function AdminDashboard() { // Feature #15: Export CSV const exportCSV = () => { + setActionLoading(true); const headers = ["Kode", "Nama", "Kategori", "Status", "Peminjam", "Dipinjam Sejak"]; const rows = filteredAssets.map(a => { const txn = getTxn(a); @@ -310,6 +315,7 @@ export default function AdminDashboard() { link.href = URL.createObjectURL(blob); link.download = `lentera-assets-${new Date().toISOString().slice(0, 10)}.csv`; link.click(); + setActionLoading(false); toast.success("Data aset berhasil di-export!"); }; @@ -446,14 +452,19 @@ export default function AdminDashboard() { setSearchQuery(e.target.value)} placeholder="Cari alat... (Ctrl+K)" className="w-full pl-10 pr-4 py-2 rounded-xl bg-[var(--card)] border border-[var(--card-border)] text-sm text-[var(--foreground)] placeholder:text-[var(--subtle)] focus:outline-none focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] transition-all" />
- - + + +
+ Menampilkan {filteredAssets.length} dari {assets.length} aset + {selectedCat !== null && <> di kategori ini} + {searchQuery && <> untuk "{searchQuery}"} +
- + @@ -463,7 +474,12 @@ export default function AdminDashboard() { {filteredAssets.length === 0 && ( - + )} {filteredAssets.map(asset => ( @@ -588,6 +604,27 @@ export default function AdminDashboard() { )} + {/* CONFIRMATION MODAL */} + setConfirmModal(null)} title="Konfirmasi" size="sm"> +
+
+ +
+

Konfirmasi {confirmModal === 'batch-qr' ? 'Download Semua QR' : 'Ekspor CSV'}

+

+ {confirmModal === 'batch-qr' + ? `Anda akan men-download QR code untuk ${filteredAssets.length} aset yang ditampilkan.` + : `Anda akan mengexport data ${filteredAssets.length} aset ke file CSV.`} +

+
+ + +
+
+
+ {/* LOGOUT CONFIRMATION MODAL */} setShowLogoutModal(false)} title="Konfirmasi Logout" size="sm">
diff --git a/lentera-frontend/components/ui/Toast.tsx b/lentera-frontend/components/ui/Toast.tsx index e01dd22..aa43d1b 100644 --- a/lentera-frontend/components/ui/Toast.tsx +++ b/lentera-frontend/components/ui/Toast.tsx @@ -100,7 +100,7 @@ export function ToastProvider({ children }: { children: ReactNode }) { }} > {children} -
+
{toasts.map((toast) => (
From 35cfe121fe3c9b05f049baa88e6e66c88c1d9626 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:22:44 +0700 Subject: [PATCH 13/28] fix: vercel.json rootDirectory buat auto-deploy preview --- vercel.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..eb5a569 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rootDirectory": "lentera-frontend" +} From 706d7d59923b8735a20292707259916467444999 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:34:51 +0700 Subject: [PATCH 14/28] hapus vercel.json (gak ngefek, di-override project setting) --- .vercel/README.txt | 11 +++++++++++ .vercel/project.json | 1 + lentera-frontend/.vercel/README.txt | 11 +++++++++++ vercel.json | 3 --- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 .vercel/README.txt create mode 100644 .vercel/project.json create mode 100644 lentera-frontend/.vercel/README.txt delete mode 100644 vercel.json diff --git a/.vercel/README.txt b/.vercel/README.txt new file mode 100644 index 0000000..525d8ce --- /dev/null +++ b/.vercel/README.txt @@ -0,0 +1,11 @@ +> Why do I have a folder named ".vercel" in my project? +The ".vercel" folder is created when you link a directory to a Vercel project. + +> What does the "project.json" file contain? +The "project.json" file contains: +- The ID of the Vercel project that you linked ("projectId") +- The ID of the user or team your Vercel project is owned by ("orgId") + +> Should I commit the ".vercel" folder? +No, you should not share the ".vercel" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..178247a --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_YpwoVilhpNOGVH3XErGd8uoCyoLL","orgId":"team_p7Y24AzkJDCLJMVSGhyG4D5a","projectName":"lentera"} \ No newline at end of file diff --git a/lentera-frontend/.vercel/README.txt b/lentera-frontend/.vercel/README.txt new file mode 100644 index 0000000..525d8ce --- /dev/null +++ b/lentera-frontend/.vercel/README.txt @@ -0,0 +1,11 @@ +> Why do I have a folder named ".vercel" in my project? +The ".vercel" folder is created when you link a directory to a Vercel project. + +> What does the "project.json" file contain? +The "project.json" file contains: +- The ID of the Vercel project that you linked ("projectId") +- The ID of the user or team your Vercel project is owned by ("orgId") + +> Should I commit the ".vercel" folder? +No, you should not share the ".vercel" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/vercel.json b/vercel.json deleted file mode 100644 index eb5a569..0000000 --- a/vercel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDirectory": "lentera-frontend" -} From 294f08234eb456387c1cd3b1bbff5e3433914d1d Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:38:20 +0700 Subject: [PATCH 15/28] bersihin .vercel files, tambah .gitignore, set rootDirectory lentera-frontend --- .gitignore | 1 + .vercel/README.txt | 11 ----------- .vercel/project.json | 1 - lentera-frontend/.vercel/README.txt | 11 ----------- 4 files changed, 1 insertion(+), 23 deletions(-) create mode 100644 .gitignore delete mode 100644 .vercel/README.txt delete mode 100644 .vercel/project.json delete mode 100644 lentera-frontend/.vercel/README.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc53f5a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.vercel diff --git a/.vercel/README.txt b/.vercel/README.txt deleted file mode 100644 index 525d8ce..0000000 --- a/.vercel/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -> Why do I have a folder named ".vercel" in my project? -The ".vercel" folder is created when you link a directory to a Vercel project. - -> What does the "project.json" file contain? -The "project.json" file contains: -- The ID of the Vercel project that you linked ("projectId") -- The ID of the user or team your Vercel project is owned by ("orgId") - -> Should I commit the ".vercel" folder? -No, you should not share the ".vercel" folder with anyone. -Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/.vercel/project.json b/.vercel/project.json deleted file mode 100644 index 178247a..0000000 --- a/.vercel/project.json +++ /dev/null @@ -1 +0,0 @@ -{"projectId":"prj_YpwoVilhpNOGVH3XErGd8uoCyoLL","orgId":"team_p7Y24AzkJDCLJMVSGhyG4D5a","projectName":"lentera"} \ No newline at end of file diff --git a/lentera-frontend/.vercel/README.txt b/lentera-frontend/.vercel/README.txt deleted file mode 100644 index 525d8ce..0000000 --- a/lentera-frontend/.vercel/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -> Why do I have a folder named ".vercel" in my project? -The ".vercel" folder is created when you link a directory to a Vercel project. - -> What does the "project.json" file contain? -The "project.json" file contains: -- The ID of the Vercel project that you linked ("projectId") -- The ID of the user or team your Vercel project is owned by ("orgId") - -> Should I commit the ".vercel" folder? -No, you should not share the ".vercel" folder with anyone. -Upon creation, it will be automatically added to your ".gitignore" file. From 1fd0e58a4e7a6fda145838713ec2ec14d69bdee5 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:51:06 +0700 Subject: [PATCH 16/28] hapus teks search result count dari dashboard --- lentera-frontend/app/admin/dashboard/page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index 22bdb44..69f836f 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -456,12 +456,6 @@ export default function AdminDashboard() {
-
- Menampilkan {filteredAssets.length} dari {assets.length} aset - {selectedCat !== null && <> di kategori ini} - {searchQuery && <> untuk "{searchQuery}"} -
-
KODE | MERK ALAT STATUS
{searchQuery ? `Tidak ada alat yang cocok dengan "${searchQuery}"` : "Tidak ada alat di kategori ini."}
+
+ +

{searchQuery ? `Tidak ada alat yang cocok dengan "${searchQuery}"` : "Tidak ada alat di kategori ini."}

+
+
From 6fc6395470da2e28c5517f2a3f4ee9183173f816 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 13:56:20 +0700 Subject: [PATCH 17/28] fix: hapus working-directory di workflow biar gak dobel path sama rootDirectory --- .github/workflows/deploy.yml | 1 - .gitignore | 3 ++- .vercel/project.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .vercel/project.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 35f430f..907bd07 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,4 +15,3 @@ jobs: - run: npm ci working-directory: lentera-frontend - run: npx vercel --prod --yes --scope mu-adz-hudzaifah-s-projects --token=${{ secrets.VERCEL_TOKEN }} - working-directory: lentera-frontend diff --git a/.gitignore b/.gitignore index cc53f5a..a34ba91 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/.vercel +/.vercel/* +!/.vercel/project.json diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..178247a --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_YpwoVilhpNOGVH3XErGd8uoCyoLL","orgId":"team_p7Y24AzkJDCLJMVSGhyG4D5a","projectName":"lentera"} \ No newline at end of file From 4d1624468c0524bccced5e03ebcecb6d691b244a Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 14:18:17 +0700 Subject: [PATCH 18/28] chore: hapus GH Actions workflow, pake native Vercel auto-deploy --- .github/workflows/deploy.yml | 17 ----------------- .gitignore | 3 +-- .vercel/project.json | 1 - 3 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .vercel/project.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 907bd07..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Deploy to Vercel Production - -on: - push: - branches: [production] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - - run: npm ci - working-directory: lentera-frontend - - run: npx vercel --prod --yes --scope mu-adz-hudzaifah-s-projects --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.gitignore b/.gitignore index a34ba91..cc53f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/.vercel/* -!/.vercel/project.json +/.vercel diff --git a/.vercel/project.json b/.vercel/project.json deleted file mode 100644 index 178247a..0000000 --- a/.vercel/project.json +++ /dev/null @@ -1 +0,0 @@ -{"projectId":"prj_YpwoVilhpNOGVH3XErGd8uoCyoLL","orgId":"team_p7Y24AzkJDCLJMVSGhyG4D5a","projectName":"lentera"} \ No newline at end of file From c5a0bbd8cded5c44717a86224b42e05bb24fa8b2 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 15:37:51 +0700 Subject: [PATCH 19/28] fix: gateway flow, field mismatch, interval settings, number input --- lentera-frontend/app/admin/dashboard/page.tsx | 4 ++-- .../app/api/assets/form-options/route.ts | 2 +- lentera-frontend/app/api/borrow/route.ts | 20 +++++++++++++---- .../app/api/gateway/generate/route.ts | 10 ++++++++- .../app/api/gateway/validate/route.ts | 2 -- lentera-frontend/app/api/return/route.ts | 22 ++++++++++++++----- lentera-frontend/app/display/page.tsx | 11 ++++++++-- 7 files changed, 54 insertions(+), 17 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index 69f836f..d8eab91 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -551,12 +551,12 @@ export default function AdminDashboard() {
- setSettingsModal({...settingsModal, qr_interval: Number(e.target.value)})} /> + { if (e.target.value === '') return; setSettingsModal({...settingsModal, qr_interval: Number(e.target.value)}) }} />

Siklus perubahan gambar barcode di layar Monitor Kiosk.

- setSettingsModal({...settingsModal, form_interval: Number(e.target.value)})} /> + { if (e.target.value === '') return; setSettingsModal({...settingsModal, form_interval: Number(e.target.value)}) }} />

Lama waktu token akses mahasiswa berlaku sebelum kadaluarsa.

diff --git a/lentera-frontend/app/api/assets/form-options/route.ts b/lentera-frontend/app/api/assets/form-options/route.ts index abcedf2..5ab39e0 100644 --- a/lentera-frontend/app/api/assets/form-options/route.ts +++ b/lentera-frontend/app/api/assets/form-options/route.ts @@ -4,7 +4,7 @@ import { createClient } from '@supabase/supabase-js'; function getSupabase() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + process.env.SUPABASE_SERVICE_ROLE_KEY! ); } diff --git a/lentera-frontend/app/api/borrow/route.ts b/lentera-frontend/app/api/borrow/route.ts index 91f3bca..ed8eb4b 100644 --- a/lentera-frontend/app/api/borrow/route.ts +++ b/lentera-frontend/app/api/borrow/route.ts @@ -11,9 +11,9 @@ function getSupabase() { export async function POST(request: Request) { const supabase = getSupabase(); const body = await request.json(); - const { asset_id, student_name, student_class, subject, submission_token } = body; + const { asset_code, student_name, student_npm, student_prodi, student_class, subject, lecturer, expected_return_at, submission_token } = body; - if (!asset_id || !student_name) { + if (!asset_code || !student_name) { return NextResponse.json({ status: 'error', message: 'Data tidak lengkap' }, { status: 400 }); } @@ -32,9 +32,19 @@ export async function POST(request: Request) { return NextResponse.json({ status: 'error', message: 'Token tidak valid' }, { status: 401 }); } + const { data: asset } = await supabase + .from('assets') + .select('id') + .eq('code', asset_code) + .single(); + + if (!asset) { + return NextResponse.json({ status: 'error', message: 'Alat tidak ditemukan' }, { status: 404 }); + } + const { data: transaction, error: txnErr } = await supabase .from('transactions') - .insert({ asset_id: Number(asset_id), student_name, student_class, subject }) + .insert({ asset_id: asset.id, student_name, student_npm, student_prodi, student_class, subject, lecturer, expected_return_at }) .select() .single(); @@ -45,7 +55,9 @@ export async function POST(request: Request) { await supabase .from('assets') .update({ status: 'borrowed', updated_at: new Date().toISOString() }) - .eq('id', asset_id); + .eq('id', asset.id); + + await supabase.from('settings').update({ value: JSON.stringify({ token: '', expires_at: 0 }) }).eq('key', 'gateway_token'); return NextResponse.json({ status: 'success', data: transaction }, { status: 201 }); } diff --git a/lentera-frontend/app/api/gateway/generate/route.ts b/lentera-frontend/app/api/gateway/generate/route.ts index 0196747..dbf0707 100644 --- a/lentera-frontend/app/api/gateway/generate/route.ts +++ b/lentera-frontend/app/api/gateway/generate/route.ts @@ -11,7 +11,15 @@ function getSupabase() { export async function GET() { const supabase = getSupabase(); const qrToken = Math.random().toString(36).substring(2, 12); - const expiresAt = Date.now() + 15 * 60 * 1000; + + const { data: settingsData } = await supabase + .from('settings') + .select('value') + .eq('key', 'form_interval') + .single(); + + const formInterval = settingsData ? Number(JSON.parse(settingsData.value)) : 15; + const expiresAt = Date.now() + formInterval * 60 * 1000; await supabase.from('settings').upsert( { key: 'gateway_token', value: JSON.stringify({ token: qrToken, expires_at: expiresAt }) }, diff --git a/lentera-frontend/app/api/gateway/validate/route.ts b/lentera-frontend/app/api/gateway/validate/route.ts index 36d4685..afa5749 100644 --- a/lentera-frontend/app/api/gateway/validate/route.ts +++ b/lentera-frontend/app/api/gateway/validate/route.ts @@ -30,8 +30,6 @@ export async function GET(request: Request) { return NextResponse.json({ status: 'error', message: 'Token tidak valid atau kadaluarsa' }, { status: 401 }); } - await supabase.from('settings').update({ value: JSON.stringify({ token: '', expires_at: 0 }) }).eq('key', 'gateway_token'); - return NextResponse.json({ status: 'success', data: { submission_token: token }, diff --git a/lentera-frontend/app/api/return/route.ts b/lentera-frontend/app/api/return/route.ts index a7fa1ef..e242aa2 100644 --- a/lentera-frontend/app/api/return/route.ts +++ b/lentera-frontend/app/api/return/route.ts @@ -11,9 +11,9 @@ function getSupabase() { export async function POST(request: Request) { const supabase = getSupabase(); const body = await request.json(); - const { asset_id, student_name, submission_token } = body; + const { asset_code, student_npm, submission_token } = body; - if (!asset_id || !student_name) { + if (!asset_code || !student_npm) { return NextResponse.json({ status: 'error', message: 'Data tidak lengkap' }, { status: 400 }); } @@ -32,11 +32,21 @@ export async function POST(request: Request) { return NextResponse.json({ status: 'error', message: 'Token tidak valid' }, { status: 401 }); } + const { data: asset } = await supabase + .from('assets') + .select('id') + .eq('code', asset_code) + .single(); + + if (!asset) { + return NextResponse.json({ status: 'error', message: 'Alat tidak ditemukan' }, { status: 404 }); + } + const { data: txn } = await supabase .from('transactions') .select('id') - .eq('asset_id', Number(asset_id)) - .eq('student_name', student_name) + .eq('asset_id', asset.id) + .eq('student_npm', student_npm) .is('returned_at', null) .single(); @@ -58,7 +68,9 @@ export async function POST(request: Request) { await supabase .from('assets') .update({ status: 'available', updated_at: new Date().toISOString() }) - .eq('id', asset_id); + .eq('id', asset.id); + + await supabase.from('settings').update({ value: JSON.stringify({ token: '', expires_at: 0 }) }).eq('key', 'gateway_token'); return NextResponse.json({ status: 'success', data: transaction }); } diff --git a/lentera-frontend/app/display/page.tsx b/lentera-frontend/app/display/page.tsx index e248455..e2419cc 100644 --- a/lentera-frontend/app/display/page.tsx +++ b/lentera-frontend/app/display/page.tsx @@ -12,6 +12,13 @@ export default function DisplayQR() { const [qrToken, setQrToken] = useState(''); const [expiresAt, setExpiresAt] = useState(''); const [error, setError] = useState(''); + const [qrInterval, setQrInterval] = useState(1); + + useEffect(() => { + api.get('/admin/settings').then(res => { + if (res.data.qr_interval) setQrInterval(res.data.qr_interval); + }).catch(() => {}); + }, []); useEffect(() => { const fetchData = async () => { @@ -30,9 +37,9 @@ export default function DisplayQR() { }; fetchData(); - const interval = setInterval(fetchData, 60000); + const interval = setInterval(fetchData, qrInterval * 60 * 1000); return () => clearInterval(interval); - }, []); + }, [qrInterval]); if (error) { return ( From ac61f64fe1881abdbc5e47032b8ca481fc9b488a Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 15:41:52 +0700 Subject: [PATCH 20/28] fix: input number bisa kosong pas backspace --- lentera-frontend/app/admin/dashboard/page.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index d8eab91..cd3bef2 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -41,7 +41,7 @@ export default function AdminDashboard() { const [catModal, setCatModal] = useState<{isOpen: boolean, type: 'add'|'edit', data: CategoryModalData}>({isOpen: false, type: 'add', data: {name: ''}}); const [assetModal, setAssetModal] = useState<{isOpen: boolean, type: 'add'|'edit', data: AssetModalData}>({isOpen: false, type: 'add', data: {category_id: '', name: '', code: ''}}); - const [settingsModal, setSettingsModal] = useState<{isOpen: boolean, qr_interval: number, form_interval: number}>(() => { + const [settingsModal, setSettingsModal] = useState<{isOpen: boolean, qr_interval: number | '', form_interval: number | ''}>(() => { const saved = typeof window !== 'undefined' ? localStorage.getItem('settings') : null; const defaults = { qr_interval: 30, form_interval: 15 }; const parsed = saved ? JSON.parse(saved) : defaults; @@ -223,14 +223,10 @@ export default function AdminDashboard() { }; const saveSettings = () => { - localStorage.setItem('settings', JSON.stringify({ - qr_interval: settingsModal.qr_interval, - form_interval: settingsModal.form_interval - })); - triggerAction('UPDATE_SETTINGS', undefined, { - qr_interval: Number(settingsModal.qr_interval), - form_interval: Number(settingsModal.form_interval) - }); + const qr = settingsModal.qr_interval === '' ? 30 : settingsModal.qr_interval; + const fi = settingsModal.form_interval === '' ? 15 : settingsModal.form_interval; + localStorage.setItem('settings', JSON.stringify({ qr_interval: qr, form_interval: fi })); + triggerAction('UPDATE_SETTINGS', undefined, { qr_interval: Number(qr), form_interval: Number(fi) }); setSettingsModal(p => ({ ...p, isOpen: false })); }; @@ -551,12 +547,12 @@ export default function AdminDashboard() {
- { if (e.target.value === '') return; setSettingsModal({...settingsModal, qr_interval: Number(e.target.value)}) }} /> + setSettingsModal({...settingsModal, qr_interval: e.target.value === '' ? '' : Number(e.target.value)})} />

Siklus perubahan gambar barcode di layar Monitor Kiosk.

- { if (e.target.value === '') return; setSettingsModal({...settingsModal, form_interval: Number(e.target.value)}) }} /> + setSettingsModal({...settingsModal, form_interval: e.target.value === '' ? '' : Number(e.target.value)})} />

Lama waktu token akses mahasiswa berlaku sebelum kadaluarsa.

From 257a073b4fc21c47105283bcdac60de56d875844 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 15:47:50 +0700 Subject: [PATCH 21/28] fix: form-options kirim asset code bukan id --- lentera-frontend/app/api/assets/form-options/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lentera-frontend/app/api/assets/form-options/route.ts b/lentera-frontend/app/api/assets/form-options/route.ts index 5ab39e0..e690ede 100644 --- a/lentera-frontend/app/api/assets/form-options/route.ts +++ b/lentera-frontend/app/api/assets/form-options/route.ts @@ -44,7 +44,7 @@ export async function GET(request: Request) { const catName = Array.isArray(cat) ? (cat[0]?.name as string) : (cat?.name as string); return { label: `${a.code} — ${a.name}`, - value: String(a.id), + value: a.code, category: catName || '', }; }); From 83a9f253aa2bc5cec50524f55de2027d1d1edf4d Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 15:57:29 +0700 Subject: [PATCH 22/28] feat: tambah migration transaction fields (npm, prodi, lecturer, expected_return) --- .../migrations/20260729000002_add_transaction_fields.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 lentera-frontend/supabase/migrations/20260729000002_add_transaction_fields.sql diff --git a/lentera-frontend/supabase/migrations/20260729000002_add_transaction_fields.sql b/lentera-frontend/supabase/migrations/20260729000002_add_transaction_fields.sql new file mode 100644 index 0000000..902f228 --- /dev/null +++ b/lentera-frontend/supabase/migrations/20260729000002_add_transaction_fields.sql @@ -0,0 +1,4 @@ +ALTER TABLE transactions ADD COLUMN IF NOT EXISTS student_npm TEXT; +ALTER TABLE transactions ADD COLUMN IF NOT EXISTS student_prodi TEXT; +ALTER TABLE transactions ADD COLUMN IF NOT EXISTS lecturer TEXT; +ALTER TABLE transactions ADD COLUMN IF NOT EXISTS expected_return_at TIMESTAMPTZ; From 5fcbc38f3f0bc302ea7c98d2656d3d390ffbd26c Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 16:07:17 +0700 Subject: [PATCH 23/28] fix: scan API maybeSingle, assets API include lastTransaction, cek-alat render fix --- lentera-frontend/app/api/assets/route.ts | 23 +++++++++++++++++- .../app/api/assets/scan/[code]/route.ts | 7 ++++-- lentera-frontend/app/cek-alat/page.tsx | 24 ++++++++++--------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lentera-frontend/app/api/assets/route.ts b/lentera-frontend/app/api/assets/route.ts index 6f93878..b7d5b65 100644 --- a/lentera-frontend/app/api/assets/route.ts +++ b/lentera-frontend/app/api/assets/route.ts @@ -10,6 +10,7 @@ function getSupabase() { export async function GET() { const supabase = getSupabase(); + const { data: assets, error } = await supabase .from('assets') .select('*, category:categories(*)') @@ -19,7 +20,27 @@ export async function GET() { return NextResponse.json({ status: 'error', message: error.message }, { status: 500 }); } - return NextResponse.json({ status: 'success', data: assets || [] }); + const result = assets || []; + + const borrowedIds = result.filter(a => a.status === 'borrowed').map(a => a.id); + if (borrowedIds.length > 0) { + const { data: transactions } = await supabase + .from('transactions') + .select('*') + .in('asset_id', borrowedIds) + .is('returned_at', null); + + if (transactions) { + for (const asset of result) { + if (asset.status === 'borrowed') { + const txn = transactions.find(t => t.asset_id === asset.id); + (asset as Record).lastTransaction = txn || null; + } + } + } + } + + return NextResponse.json({ status: 'success', data: result }); } export async function POST(request: Request) { diff --git a/lentera-frontend/app/api/assets/scan/[code]/route.ts b/lentera-frontend/app/api/assets/scan/[code]/route.ts index 0f43b07..0ac3ccf 100644 --- a/lentera-frontend/app/api/assets/scan/[code]/route.ts +++ b/lentera-frontend/app/api/assets/scan/[code]/route.ts @@ -29,10 +29,13 @@ export async function GET(_request: Request, { params }: { params: Promise<{ cod .is('returned_at', null) .order('borrowed_at', { ascending: false }) .limit(1) - .single(); + .maybeSingle(); return NextResponse.json({ status: 'success', - data: { ...data, lastTransaction: transaction || null }, + data: { + ...data, + lastTransaction: transaction || null, + }, }); } diff --git a/lentera-frontend/app/cek-alat/page.tsx b/lentera-frontend/app/cek-alat/page.tsx index 62b23da..bb536f2 100644 --- a/lentera-frontend/app/cek-alat/page.tsx +++ b/lentera-frontend/app/cek-alat/page.tsx @@ -10,17 +10,19 @@ import Input from '@/components/ui/Input'; import { Asset, Category } from '@/lib/types'; import { isApiStatus } from '@/lib/api'; +interface TransactionData { + student_name: string; + student_class?: string; + subject?: string; + borrowed_at: string; +} + interface ScanData { category: string; name: string; code: string; status: 'available' | 'borrowed' | 'maintenance'; - borrower?: { - name: string; - class?: string; - subject?: string; - borrowed_at: string; - }; + lastTransaction?: TransactionData | null; } function CekAlatContent() { @@ -173,14 +175,14 @@ function CekAlatContent() {
{statusBadge(scanData.status)}
- {scanData.status === 'borrowed' && scanData.borrower && ( + {scanData.status === 'borrowed' && scanData.lastTransaction && (

Sedang Dipinjam Oleh:

-
Nama{scanData.borrower.name}
-
Kelas{scanData.borrower.class}
-
Matkul{scanData.borrower.subject}
-
Sejak{scanData.borrower.borrowed_at}
+
Nama{scanData.lastTransaction.student_name}
+
Kelas{scanData.lastTransaction.student_class}
+
Matkul{scanData.lastTransaction.subject}
+
Sejak{scanData.lastTransaction.borrowed_at}
)} From 432ad8ad6c474d66b820674b90e85d9426239c78 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 16:19:07 +0700 Subject: [PATCH 24/28] fix: category type, format WIB, time 24h --- lentera-frontend/app/admin/dashboard/page.tsx | 13 +++++++++- lentera-frontend/app/cek-alat/page.tsx | 24 ++++++++++++++++--- lentera-frontend/app/form/borrow/page.tsx | 1 + 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index cd3bef2..b066358 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -15,6 +15,17 @@ import Skeleton from '@/components/ui/Skeleton'; import { QRCodeCanvas } from 'qrcode.react'; import { Asset, Category, PendingAction, CategoryModalData, AssetModalData, QrModalData } from '@/lib/types'; +function formatWIB(iso: string): string { + const d = new Date(iso); + const dateStr = d.toLocaleDateString('id-ID', { + timeZone: 'Asia/Jakarta', weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' + }); + const timeStr = d.toLocaleTimeString('id-ID', { + timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit', hour12: false + }); + return `${dateStr}, ${timeStr} WIB`; +} + export default function AdminDashboard() { const router = useRouter(); const toast = useToast(); @@ -484,7 +495,7 @@ export default function AdminDashboard() { {(() => { const txn = getTxn(asset); return asset.status === 'borrowed' && txn ? ( -
{txn.student_name}
{txn.borrowed_at}
+
{txn.student_name}
{formatWIB(txn.borrowed_at)}
) : ( - ); diff --git a/lentera-frontend/app/cek-alat/page.tsx b/lentera-frontend/app/cek-alat/page.tsx index bb536f2..29effb0 100644 --- a/lentera-frontend/app/cek-alat/page.tsx +++ b/lentera-frontend/app/cek-alat/page.tsx @@ -17,14 +17,32 @@ interface TransactionData { borrowed_at: string; } +interface CategoryData { + id: number; + name: string; +} + interface ScanData { - category: string; + category: string | CategoryData; name: string; code: string; status: 'available' | 'borrowed' | 'maintenance'; lastTransaction?: TransactionData | null; } +const catName = (cat: string | CategoryData) => typeof cat === 'string' ? cat : cat.name; + +function formatWIB(iso: string): string { + const d = new Date(iso); + const dateStr = d.toLocaleDateString('id-ID', { + timeZone: 'Asia/Jakarta', weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' + }); + const timeStr = d.toLocaleTimeString('id-ID', { + timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit', hour12: false + }); + return `${dateStr}, ${timeStr} WIB`; +} + function CekAlatContent() { const searchParams = useSearchParams(); const router = useRouter(); @@ -167,7 +185,7 @@ function CekAlatContent() {
-
{scanData.category}
+
{catName(scanData.category)}

{scanData.name}

{scanData.code} @@ -182,7 +200,7 @@ function CekAlatContent() {
Nama{scanData.lastTransaction.student_name}
Kelas{scanData.lastTransaction.student_class}
Matkul{scanData.lastTransaction.subject}
-
Sejak{scanData.lastTransaction.borrowed_at}
+
Sejak{formatWIB(scanData.lastTransaction.borrowed_at)}
)} diff --git a/lentera-frontend/app/form/borrow/page.tsx b/lentera-frontend/app/form/borrow/page.tsx index 0e8e19c..6bd69cc 100644 --- a/lentera-frontend/app/form/borrow/page.tsx +++ b/lentera-frontend/app/form/borrow/page.tsx @@ -185,6 +185,7 @@ function BorrowFormContent() { value={formData.return_time} onChange={handleChange} required + step="60" />
From 1bdfa5a1293dc81bf5217498c514aec14e12d66c Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 16:31:13 +0700 Subject: [PATCH 25/28] fix: return form glitch + WIB + redirect, sudo toast, CSV WIB --- lentera-frontend/app/admin/dashboard/page.tsx | 6 +++--- lentera-frontend/app/api/gateway/generate/route.ts | 1 + lentera-frontend/app/form/return/page.tsx | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lentera-frontend/app/admin/dashboard/page.tsx b/lentera-frontend/app/admin/dashboard/page.tsx index b066358..4ead5c9 100644 --- a/lentera-frontend/app/admin/dashboard/page.tsx +++ b/lentera-frontend/app/admin/dashboard/page.tsx @@ -120,9 +120,9 @@ export default function AdminDashboard() { if (pendingAction) { await executeAction(pendingAction.type, pendingAction.id, pendingAction.payload); + } else { + toast.success(res.data.message || "Otorisasi berhasil!"); } - - toast.success(res.data.message || "Aksi berhasil dieksekusi!"); setSudoPassword(''); setShowSudoModal(false); } catch (err: unknown) { @@ -313,7 +313,7 @@ export default function AdminDashboard() { a.category?.name || '-', a.status, txn?.student_name || '-', - txn?.borrowed_at || '-' + (txn ? formatWIB(txn.borrowed_at) : '-') ]; }); const csv = [headers, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n'); diff --git a/lentera-frontend/app/api/gateway/generate/route.ts b/lentera-frontend/app/api/gateway/generate/route.ts index dbf0707..ce50af5 100644 --- a/lentera-frontend/app/api/gateway/generate/route.ts +++ b/lentera-frontend/app/api/gateway/generate/route.ts @@ -27,6 +27,7 @@ export async function GET() { ); const expiresAtStr = new Date(expiresAt).toLocaleString('id-ID', { + timeZone: 'Asia/Jakarta', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); diff --git a/lentera-frontend/app/form/return/page.tsx b/lentera-frontend/app/form/return/page.tsx index 50002a6..69d016b 100644 --- a/lentera-frontend/app/form/return/page.tsx +++ b/lentera-frontend/app/form/return/page.tsx @@ -16,7 +16,7 @@ function ReturnFormContent() { const submissionToken = searchParams.get('submission_token'); const [options, setOptions] = useState([]); - const [loadingOptions, setLoadingOptions] = useState(!submissionToken); // true if no token + const [loadingOptions, setLoadingOptions] = useState(true); const [error, setError] = useState(submissionToken ? '' : 'Akses ditolak. Token sesi tidak ditemukan.'); const [formData, setFormData] = useState({ @@ -52,7 +52,7 @@ function ReturnFormContent() { await api.post('/return', payload); toast.success('Pengembalian berhasil! Terima kasih.'); - router.push('/'); + router.push(`/cek-alat?code=${encodeURIComponent(formData.asset_code)}`); } catch (err: unknown) { toast.error(getApiErrorMessage(err, 'Pengembalian gagal.')); } @@ -93,7 +93,7 @@ function ReturnFormContent() {

Form Pengembalian Alat

- {options.length === 0 ? ( + {options.length === 0 && !loadingOptions ? (

Tidak ada alat yang sedang dipinjam saat ini.

From 2ceabcd77717944b19038e51f1c0e0c7fae0e141 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 17:00:55 +0700 Subject: [PATCH 26/28] chore: migrate supabase sydney to singapore --- lentera-frontend/supabase/dump-full.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lentera-frontend/supabase/dump-full.sql diff --git a/lentera-frontend/supabase/dump-full.sql b/lentera-frontend/supabase/dump-full.sql new file mode 100644 index 0000000..e69de29 From 3d8e002d9090161f563e8fb2ae86b559ba649d93 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 17:01:06 +0700 Subject: [PATCH 27/28] chore: cleanup dump files --- lentera-frontend/supabase/dump-full.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lentera-frontend/supabase/dump-full.sql diff --git a/lentera-frontend/supabase/dump-full.sql b/lentera-frontend/supabase/dump-full.sql deleted file mode 100644 index e69de29..0000000 From 41400a67f715819f9697eecae24d5b29369a8606 Mon Sep 17 00:00:00 2001 From: whoami Date: Wed, 29 Jul 2026 17:42:49 +0700 Subject: [PATCH 28/28] docs: update readme with project structure & tech stack --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0360cbf..a3508ec 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,80 @@ -# Lentera - Lending & Tracking Application +# Lentera — Lending & Tracking Application -A simple and secure laboratory asset lending and tracking system. This project was developed to fulfill the exam practicum assignment and later adapted for production deployment. +Laboratory asset lending and tracking system with kiosk-based self-service borrowing and return. ## Tech Stack -- **Frontend:** Next.js 16, React 19, TypeScript -- **Styling:** Tailwind CSS v4 -- **Database & Auth:** Supabase (PostgreSQL) -- **API Layer:** Vercel Serverless Functions (Next.js API Routes) -- **Deployment:** Vercel -- **Original Backend:** Laravel -- **Libraries:** Axios, bcryptjs, qrcode.react, @vercel/analytics +| Layer | Technology | +|-------|-----------| +| **Frontend** | Next.js 16, React 19, TypeScript | +| **Styling** | Tailwind CSS v4 | +| **API Layer** | Next.js Route Handlers (Vercel Serverless Functions) — inside `lentera-frontend/app/api/` | +| **Database** | Supabase (PostgreSQL) | +| **Deployment** | Vercel | + +**Note:** The `lentera-backend/` directory contains an abandoned Laravel prototype used only as a reference during initial development. It is NOT used in production. All API logic lives in the Next.js route handlers (`lentera-frontend/app/api/`). + +## Project Structure + +``` +lentera/ +├── lentera-frontend/ # Next.js web app (the actual application) +│ ├── app/ +│ │ ├── api/ # Serverless API route handlers +│ │ │ ├── assets/ # Asset CRUD + scan +│ │ │ ├── borrow/ # Borrow workflow +│ │ │ ├── return/ # Return workflow +│ │ │ ├── gateway/ # Kiosk QR token generate & validate +│ │ │ ├── categories/ # Category CRUD +│ │ │ ├── admin/ # Admin settings & sudo +│ │ │ ├── login/ # Authentication +│ │ │ └── logout/ # Session end +│ │ ├── (admin/) # Admin dashboard, print labels +│ │ ├── (cek-alat/) # Public asset check page +│ │ ├── (display/) # Kiosk QR display monitor +│ │ ├── (form/borrow/) # Borrow form (kiosk) +│ │ ├── (form/return/) # Return form (kiosk) +│ │ └── (scan/) # QR scan landing page +│ ├── components/ # Reusable React components (UI kit) +│ ├── lib/ # API client, types, utilities +│ └── supabase/ # DB migrations & CLI config +│ └── migrations/ # SQL migration files +├── lentera-backend/ # Laravel prototype (unused / reference only) +└── README.md +``` + +## Flow + +1. **Admin** manages assets, categories, and settings via the dashboard (`/admin/dashboard`). +2. **Kiosk monitor** (`/display`) shows a rotating QR code with a gateway token. +3. **Student** scans the QR → `/scan` validates the token → picks **Pinjam** or **Kembalikan**. +4. **Borrow** (`/form/borrow`): Student fills form (name, NPM, subject, lecturer, return date) → submits → asset status set to `borrowed`. +5. **Return** (`/form/return`): Student selects borrowed asset + enters NPM → submits → asset status back to `available`. + +## Database + +Hosted on **Supabase** (PostgreSQL). Schema migrations in `lentera-frontend/supabase/migrations/`: + +- `20260729000001_init.sql` — Tables: `admins`, `categories`, `assets`, `transactions`, `settings` +- `20260729000002_add_transaction_fields.sql` — Adds `student_npm`, `student_prodi`, `lecturer`, `expected_return_at` + +## Development + +```bash +cd lentera-frontend +npm install +npm run dev +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL | +| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key | +| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key (API routes) | ## Developed By -- **Backend:** [@rannd1nt](https://github.com/rannd1nt) (Zahraan Dzakii Ts.) -- **Frontend:** [@MuadzHdz](https://github.com/MuadzHdz) (Mu'adz Hudzaifah) +- **Backend (original Laravel prototype):** [@rannd1nt](https://github.com/rannd1nt) (Zahraan Dzakii Ts.) +- **Frontend & Current Backend (Next.js + Supabase):** [@MuadzHdz](https://github.com/MuadzHdz) (Mu'adz Hudzaifah)