first commit

This commit is contained in:
Intan Guci 2026-05-27 18:02:21 +07:00
commit 29525cba4e
60 changed files with 7132 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
README.md Normal file
View File

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

22
eslint.config.js Normal file
View File

@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>kasir</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3981
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "kasir",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.16.0",
"lucide-react": "^1.16.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"postcss": "^8.5.12",
"tailwindcss": "^3.4.19",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

184
src/App.css Normal file
View File

@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

55
src/App.tsx Normal file
View File

@ -0,0 +1,55 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Dashboard from "./routes/userDashboard";
import MasterData from "./routes/userMaster-Data";
import Transaksi from "./routes/usertransaksi";
import Laporan from "./routes/userLaporan";
import Settings from "./routes/userSettings";
function App() {
return (
<BrowserRouter>
<Routes>
{/* Redirect awal */}
<Route
path="/"
element={<Navigate to="/userDashboard" />}
/>
{/* DASHBOARD */}
<Route
path="/userDashboard"
element={<Dashboard />}
/>
{/* MASTER DATA */}
<Route
path="/userDashboard/master-data"
element={<MasterData />}
/>
{/* TRANSAKSI */}
<Route
path="/userDashboard/transaksi"
element={<Transaksi />}
/>
{/* LAPORAN */}
<Route
path="/userDashboard/laporan"
element={<Laporan />}
/>
{/* SETTINGS */}
<Route
path="/userDashboard/settings"
element={<Settings />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;

24
src/Routes.tsx Normal file
View File

@ -0,0 +1,24 @@
// src/Routes.tsx
import { Routes, Route } from "react-router-dom";
import UserDashboardLayout from "./layout/userDashboardLayout";
import UserDashboard from "./routes/userDashboard";
import UserLaporan from "./routes/userLaporan";
import UserBAST from "./routes/userBAST";
export default function AppRoutes() {
return (
<Routes>
{/* route dengan sidebar */}
<Route element={<UserDashboardLayout />}>
<Route path="/" element={<UserDashboard />} />
<Route path="/laporan" element={<UserLaporan />} />
<Route path="/bast" element={<UserBAST />} />
</Route>
</Routes>
);
}

BIN
src/assets/Filsafat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
src/assets/alat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

BIN
src/assets/buku.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

BIN
src/assets/elektronik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/komputer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/assets/lampu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/laptop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
src/assets/matematika.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
src/assets/novel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
src/assets/pensil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
src/assets/pulang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
src/assets/pulpen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

BIN
src/assets/setrika.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
src/assets/tas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

BIN
src/assets/tipex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
src/assets/welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,13 @@
export default function About() {
return (
<div className="bg-white rounded-2xl shadow p-6 mb-8">
<h2 className="text-2xl font-bold mb-3">
Tentang Sistem
</h2>
<p className="text-gray-600">
Aplikasi kasir dan peminjaman barang sekolah ini dirancang untuk mempermudah pengelolaan transaksi penjualan, pencatatan data barang, serta proses peminjaman perlengkapan sekolah seperti buku, alat tulis, dan perangkat pendukung lainnya. Dengan sistem yang modern dan terintegrasi, pengguna dapat mengelola stok barang, melakukan transaksi, memantau data peminjaman, serta melihat laporan dengan lebih cepat, akurat, dan aman. Dashboard utama memberikan akses mudah ke seluruh fitur penting sehingga operasional sekolah menjadi lebih terorganisir, efisien, dan profesional.
</p>
</div>
);
}

View File

@ -0,0 +1,61 @@
import novel from "../../assets/novel.png";
import elektronik from "../../assets/elektronik.png";
import alat from "../../assets/alat.png";
export default function Content() {
const items = [
{
name: "novel",
image: novel,
function:
"rekreatif (hiburan), didaktif (mendidik), moralitas (penanaman nilai baik-buruk), religius (ajaran agama), dan estetis (keindahan bahasa/cerita).",
},
{
name: "elektronik",
image: elektronik,
function:
"Elektronik digunakan untuk mempermudah pekerjaan manusia, memberikan hiburan, membantu komunikasi, serta meningkatkan efisiensi dan keamanan dalam kehidupan sehari-hari.",
},
{
name: "Alat Tulis",
image: alat ,
function:
"mencatat informasi, mendukung administrasi perkantoran, menunjang kegiatan belajar-mengajar, serta memfasilitasi ekspresi kreatif seperti menggambar dan merancang.",
},
];
return (
<div className="space-y-8 bg-white p-6 rounded-2xl">
<h2 className="text-3xl font-bold text-black mb-6 border-b-4 border-blue-600 pb-2">
Barang Barang
</h2>
{items.map((item, index) => (
<div
key={index}
className="bg-black text-white rounded-2xl shadow-lg p-6 flex items-center gap-8 border-l-4 border-blue-600"
>
{/* KIRI - PENJELASAN */}
<div className="flex-1">
<h3 className="text-2xl font-bold text-blue-400 mb-3">
{item.name}
</h3>
<p className="text-gray-300 leading-relaxed">
{item.function}
</p>
</div>
{/* KANAN - FOTO */}
<div className="w-72">
<img
src={item.image}
alt={item.name}
className="w-full h-56 object-cover rounded-xl shadow border-2 border-blue-500"
/>
</div>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,198 @@
// src/components/dashboard/Hero.tsx
import { useEffect, useState } from "react";
export default function Hero() {
// State untuk menyimpan data profile secara dinamis
const [userData, setUserData] = useState({ nama_lengkap: "User", role: "User" });
// State untuk menyimpan angka statistik dari database
const [stats, setStats] = useState({
permintaanHariIni: 0,
menungguPersetujuan: 0,
totalBarang: 0,
stokHampirHabis: 0,
totalPengguna: 0,
});
useEffect(() => {
// 1. AMBIL DATA USER YANG LOGIN (SINKRONISASI NYA DI SINI YA BANG)
const localName = localStorage.getItem("user_name");
const localRole = localStorage.getItem("user_role");
const sessionUser = sessionStorage.getItem("user");
if (sessionUser) {
// Jika ada data di session storage (Objek utuh)
try {
const parsedUser = JSON.parse(sessionUser);
setUserData({
nama_lengkap: parsedUser.nama_lengkap || "User",
role: parsedUser.role || "User"
});
} catch (e) {
console.error(e);
}
} else if (localName) {
// Jika data session kosong, ambil dari local storage pecahan milik login Abang
setUserData({
nama_lengkap: localName,
role: localRole || "User"
});
}
// 2. Ambil data Barang dari backend untuk Card 3
fetch("http://localhost:5000/api/barang")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
const daftarBarang = resData.data;
const hampirHabis = daftarBarang.filter((b: any) => b.stok <= 3).length;
setStats((prev) => ({
...prev,
totalBarang: daftarBarang.length,
stokHampirHabis: hampirHabis,
}));
}
})
.catch((err) => console.error("Gagal mengambil data barang:", err));
// 3. Ambil data BAST dari backend untuk Card 1 dan Card 2
fetch("http://localhost:5000/api/bast")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
const daftarBast = resData.data;
// Hitung data BAST yang statusnya masih 'Menunggu' (Card 2)
const menunggu = daftarBast.filter(
(b: any) => b.status_serah === "Menunggu" || b.status_terima === "Menunggu"
).length;
// Hitung data BAST yang dibuat HARI INI (Card 1)
const hariIniWIB = new Date().toLocaleDateString("id-ID");
const totalHariIni = daftarBast.filter((b: any) => {
if (!b.dibuat_pada) return false;
const tanggalBast = new Date(b.dibuat_pada).toLocaleDateString("id-ID");
return tanggalBast === hariIniWIB;
}).length;
setStats((prev) => ({
...prev,
menungguPersetujuan: menunggu,
permintaanHariIni: totalHariIni
}));
}
})
.catch((err) => console.error("Gagal mengambil data BAST:", err));
// 4. Ambil data Users dari backend untuk Card 4
fetch("http://localhost:5000/api/users")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
setStats((prev) => ({
...prev,
totalPengguna: resData.data.length
}));
}
})
.catch((err) => console.error("Gagal mengambil data users:", err));
}, []);
// Mengambil huruf pertama untuk avatar icon (kasih pengaman biar ga crash pas loading)
const inisialNama = userData.nama_lengkap ? userData.nama_lengkap.charAt(0).toUpperCase() : "U";
return (
<div className="bg-blue-50 rounded-3xl p-8 shadow-sm border border-blue-100 mb-8">
{/* HEADER */}
<div className="flex justify-between items-start mb-8">
{/* LEFT */}
<div>
<p className="text-gray-500 text-lg">
Selamat datang kembali 👋
</p>
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
Dashboard
</h1>
</div>
{/* PROFILE */}
<div className="flex items-center gap-3 bg-white rounded-full px-4 py-2 shadow border hover:shadow-lg transition">
{/* AVATAR */}
<div className="w-12 h-12 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
{inisialNama}
</div>
{/* USER INFO */}
<div>
<h3 className="font-semibold text-black">
{userData.nama_lengkap}
</h3>
<p className="text-gray-500 text-sm">
{userData.role}
</p>
</div>
</div>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-4 gap-6">
{/* CARD 1 */}
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
<p className="text-gray-500 mb-2">
Permintaan Hari Ini
</p>
<h2 className="text-4xl font-bold text-blue-700">
{stats.permintaanHariIni}
</h2>
<p className="text-green-600 mt-2">
Dokumen BAST baru
</p>
</div>
{/* CARD 2 */}
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
<p className="text-gray-500 mb-2">
Menunggu Persetujuan
</p>
<h2 className="text-4xl font-bold text-blue-700">
{stats.menungguPersetujuan}
</h2>
<p className="text-yellow-600 mt-2">
Perlu tindakan segera
</p>
</div>
{/* CARD 3 */}
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
<p className="text-gray-500 mb-2">
Total Barang
</p>
<h2 className="text-4xl font-bold text-blue-700">
{stats.totalBarang}
</h2>
<p className="text-green-600 mt-2">
{stats.stokHampirHabis} stok hampir habis
</p>
</div>
{/* CARD 4 */}
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
<p className="text-gray-500 mb-2">
Total Pengguna
</p>
<h2 className="text-4xl font-bold text-blue-700">
{stats.totalPengguna}
</h2>
<p className="text-green-600 mt-2">
Siswa & petugas terdaftar
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import type { LucideIcon } from 'lucide-react';
interface Props {
label: string;
name: string;
type: string;
icon: LucideIcon;
placeholder: string;
showEye?: boolean;
toggleEye?: () => void;
eyeIcon?: LucideIcon;
// --- TAMBAHKAN DUA INI ---
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function LoginInput({
label, name, type, icon: Icon, placeholder, showEye, toggleEye, eyeIcon: EyeIcon,
value, onChange // <--- Terima props di sini
}: Props) {
return (
<div className="mb-4">
<label className="block text-gray-400 text-sm mb-1">{label}:</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 group-focus-within:text-[#4A90E2]">
<Icon size={20} />
</div>
<input
name={name}
type={type}
placeholder={placeholder}
// --- PASANG DI SINI ---
value={value}
onChange={onChange}
className="w-full bg-[#3D444D] border border-gray-600 rounded-lg pl-11 pr-10 py-3 text-white focus:outline-none focus:border-[#4A90E2] transition-all placeholder:text-gray-500"
/>
{showEye && toggleEye && EyeIcon && (
<button type="button" onClick={toggleEye} className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white">
<EyeIcon size={20} />
</button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,10 @@
export default function MyButton({ text, type = "button" }: { text: string; type?: "submit" | "button" }) {
return (
<button
type={type}
className="w-full bg-[#4A90E2] hover:bg-[#357ABD] text-white font-semibold py-3 rounded-lg transition-all active:scale-95 shadow-lg mt-4"
>
{text}
</button>
);
}

View File

@ -0,0 +1,28 @@
// src/components/manajemenUsers/UsersFilter.tsx
import { useState } from "react";
export default function UsersFilter() {
const [selectedRole, setSelectedRole] =
useState("Semua");
const roles = ["Semua", "Siswa"];
return (
<div className="flex gap-4 mb-6">
{roles.map((role) => (
<button
key={role}
onClick={() => setSelectedRole(role)}
className={`px-4 py-2 rounded-lg font-medium transition ${
selectedRole === role
? "bg-blue-600 text-white"
: "bg-white border hover:bg-gray-100"
}`}
>
{role}
</button>
))}
</div>
);
}

View File

@ -0,0 +1,20 @@
export default function UsersHero() {
return (
<div className="bg-blue-50 rounded-3xl p-8 shadow-sm border border-blue-100 mb-8">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 text-lg">
Kelola data pengguna sekolah 👥
</p>
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
Manajemen Users
</h1>
</div>
<button className="bg-blue-600 text-white px-6 py-3 rounded-xl font-semibold shadow hover:bg-blue-700 transition">
+ Tambah User
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,150 @@
// src/components/manajemenUsers/UsersTable.tsx
import { useState } from "react";
export default function UsersTable() {
const [search, setSearch] = useState("");
const users = [
{
id: "#S001",
nama: "Sulthan",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S002",
nama: "Zahir",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S003",
nama: "Arief",
role: "Siswa",
kelas: "XI RPL",
status: "Pending",
},
{
id: "#S004",
nama: "Muhammad",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S005",
nama: "Fajar",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S006",
nama: "Akbar",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S007",
nama: "Yudha",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S008",
nama: "Heru",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
];
// FILTER SEARCH
const filteredUsers = users.filter((user) =>
user.nama
.toLowerCase()
.includes(search.toLowerCase())
);
return (
<div className="bg-white rounded-3xl shadow-lg p-6 border">
{/* HEADER */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-black">
Data Siswa
</h2>
<input
type="text"
placeholder="Cari nama siswa..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="border border-gray-300 rounded-xl px-4 py-2 w-72 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* TABLE HEADER */}
<div className="grid grid-cols-6 bg-gray-100 p-4 rounded-xl font-semibold text-gray-700 mb-4">
<div>ID</div>
<div>Nama</div>
<div>Role</div>
<div>Kelas</div>
<div>Status</div>
<div>Aksi</div>
</div>
{/* TABLE BODY */}
<div className="space-y-3">
{filteredUsers.map((user, index) => (
<div
key={user.id}
className={`grid grid-cols-6 items-center p-4 rounded-xl border ${
index % 2 === 0
? "bg-white"
: "bg-blue-50"
}`}
>
<div>{user.id}</div>
<div>{user.nama}</div>
<div>{user.role}</div>
<div>{user.kelas}</div>
<div>
<span
className={`px-3 py-1 rounded-full text-sm ${
user.status === "Aktif"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{user.status}
</span>
</div>
<div className="flex gap-2">
<button className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">
Edit
</button>
<button className="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
Hapus
</button>
</div>
</div>
))}
</div>
{/* EMPTY */}
{filteredUsers.length === 0 && (
<div className="text-center py-8 text-gray-500">
Siswa tidak ditemukan.
</div>
)}
</div>
);
}

View File

@ -0,0 +1,98 @@
// src/components/masterData/BarangFilter.tsx
import { useState, useEffect } from "react";
type Props = {
selectedKategori: string;
setSelectedKategori: (kategori: string) => void;
searchQuery: string;
setSearchQuery: (query: string) => void;
selectedStatus: string;
setSelectedStatus: (status: string) => void;
};
type KategoriType = {
kategori_id: number;
nama_kategori: string;
};
export default function BarangFilter({
selectedKategori,
setSelectedKategori,
searchQuery,
setSearchQuery,
selectedStatus,
setSelectedStatus,
}: Props) {
// Data cadangan kategori sesuai gambar dashboard admin
const [kategoriList, setKategoriList] = useState<string[]>([
"Semua",
"Buku dan Modul",
"Alat_Kebersihan",
"Peralatan Bengkel",
"Elektronik",
"Alat Tulis",
]);
useEffect(() => {
fetch("http://localhost:5000/api/kategori")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data) && resData.data.length > 0) {
const namaKategoriDariDb = resData.data.map((k: KategoriType) => k.nama_kategori);
setKategoriList(["Semua", ...namaKategoriDariDb]);
}
})
.catch((err) => console.warn("Menggunakan kategori default.", err));
}, []);
return (
<div className="space-y-4 mb-8">
{/* BARIS 1: INPUT SEARCH DAN DROPDOWN STATUS */}
<div className="flex flex-col md:flex-row gap-4">
{/* INPUT SEARCH */}
<div className="flex-1 relative">
<input
type="text"
placeholder="Cari nama atau kode barang..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-4 pr-4 py-3 bg-white border border-gray-300 rounded-xl text-black focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm font-medium"
/>
</div>
{/* DROPDOWN FILTER STATUS KONDISI SESUAI DATABASE */}
<div className="w-full md:w-52">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl text-black focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm font-semibold"
>
<option value="Semua">Semua Kondisi</option>
<option value="Baik">Kondisi: Baik</option>
<option value="Rusak Ringan">Kondisi: Rusak Ringan</option>
<option value="Rusak Berat">Kondisi: Rusak Berat</option>
<option value="Hilang">Kondisi: Hilang</option>
</select>
</div>
</div>
{/* BARIS 2: TOMBOL FILTER KATEGORI */}
<div className="flex gap-3 flex-wrap">
{kategoriList.map((kategori) => (
<button
key={kategori}
type="button"
onClick={() => setSelectedKategori(kategori)}
className={`px-4 py-2 rounded-xl font-medium text-sm transition ${
selectedKategori === kategori
? "bg-blue-600 text-white shadow-sm"
: "bg-white border border-gray-200 text-gray-700 hover:bg-gray-50"
}`}
>
{kategori}
</button>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,74 @@
type BarangType = {
barang_id: number;
kode_barang: string;
nama_barang: string;
foto_barang: string;
status_barang: string;
stok: number;
nama_kategori: string;
nama_lokasi: string;
qr_image?: string;
};
type BarangGridProps = {
barangList: BarangType[];
onPinjam: (barang: BarangType) => void;
};
export default function BarangGrid({ barangList, onPinjam }: BarangGridProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 mt-6">
{barangList.map((barang) => (
<div
key={barang.barang_id}
className="bg-white rounded-2xl p-4 shadow border hover:shadow-lg transition flex flex-col justify-between"
>
<div>
{/* FOTO BARANG */}
<div className="w-full h-48 bg-gray-100 rounded-xl overflow-hidden mb-4 border">
<img
src={barang.foto_barang || "https://via.placeholder.com/150"}
alt={barang.nama_barang}
className="w-full h-full object-cover"
/>
</div>
{/* INFO BARANG */}
<span className="px-2.5 py-1 bg-blue-50 text-blue-700 rounded-md text-xs font-semibold uppercase">
{barang.nama_kategori}
</span>
<h3 className="font-bold text-gray-800 text-lg mt-2 line-clamp-1">
{barang.nama_barang}
</h3>
<p className="text-xs text-gray-500 mt-1">
Kode: {barang.kode_barang} | Lokasi: {barang.nama_lokasi}
</p>
<div className="flex justify-between items-center mt-4 pt-3 border-t">
<div>
<p className="text-xs text-gray-400">Status</p>
<span className={`text-sm font-bold ${barang.status_barang === 'Baik' ? 'text-green-600' : 'text-red-500'}`}>
{barang.status_barang}
</span>
</div>
<div className="text-right">
<p className="text-xs text-gray-400">Stok Tersedia</p>
<p className="text-sm font-bold text-gray-700">{barang.stok} Pcs</p>
</div>
</div>
</div>
{/* TOMBOL AKSI PINJAM */}
<button
onClick={() => onPinjam(barang)}
className="w-full mt-4 bg-blue-600 text-white py-2.5 rounded-xl font-semibold hover:bg-blue-700 transition shadow-sm"
>
Pinjam Barang
</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,13 @@
export default function BarangHero() {
return (
<div className="bg-gradient-to-r from-black via-blue-900 to-blue-600 rounded-3xl p-8 text-white mb-8">
<h1 className="text-4xl font-bold mb-2">
Kelola Barang Sekolah
</h1>
<p className="text-gray-200">
Atur stok, harga, dan data barang sekolah
dengan mudah.
</p>
</div>
);
}

View File

@ -0,0 +1,146 @@
// src/components/masterData/EditBarangModal.tsx
import { useState } from "react";
type BarangType = {
id: number;
nama: string;
foto: string;
kondisi: string;
tersedia: boolean;
kategori: string;
};
type Props = {
barang: BarangType;
onClose: () => void;
onSave: (barang: BarangType) => void;
};
export default function EditBarangModal({
barang,
onClose,
onSave,
}: Props) {
const [kondisi, setKondisi] = useState(
barang.kondisi
);
const [tersedia, setTersedia] =
useState(barang.tersedia);
const handleSubmit = () => {
onSave({
...barang,
kondisi,
tersedia,
});
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
{/* MODAL BOX */}
<div className="bg-white w-[420px] rounded-3xl shadow-2xl p-5">
{/* HEADER */}
<h2 className="text-xl font-bold text-center mb-4">
Edit Barang
</h2>
{/* FOTO */}
<div className="w-28 h-28 mx-auto rounded-2xl overflow-hidden mb-3 bg-gray-100">
<img
src={barang.foto}
alt={barang.nama}
className="w-full h-full object-cover"
/>
</div>
{/* NAMA */}
<p className="text-center font-semibold mb-1">
{barang.nama}
</p>
{/* KATEGORI */}
<p className="text-center text-sm text-blue-600 mb-4">
{barang.kategori}
</p>
{/* FORM */}
<div className="space-y-3">
{/* KONDISI */}
<div>
<label className="block text-sm font-medium mb-1">
Kondisi
</label>
<select
value={kondisi}
onChange={(e) =>
setKondisi(
e.target.value
)
}
className="w-full border rounded-lg p-2"
>
<option value="Bagus">
Bagus
</option>
<option value="Kurang Bagus">
Kurang Bagus
</option>
</select>
</div>
{/* STATUS */}
<div>
<label className="block text-sm font-medium mb-1">
Status
</label>
<select
value={
tersedia
? "true"
: "false"
}
onChange={(e) =>
setTersedia(
e.target.value ===
"true"
)
}
className="w-full border rounded-lg p-2"
>
<option value="true">
Bisa Dipinjam
</option>
<option value="false">
Tidak Tersedia
</option>
</select>
</div>
</div>
{/* BUTTON */}
<div className="flex justify-end gap-3 mt-5">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg bg-gray-300 hover:bg-gray-400"
>
Batal
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
>
pinjam
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
>
kembalikan
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,133 @@
// src/components/masterData/PinjamBarangModal.tsx
import { useState, useEffect } from "react";
import { type BarangType } from "../../routes/userMaster-Data";
type PinjamModalProps = {
barang: BarangType;
onClose: () => void;
onSuccess: () => void;
};
type UserType = {
user_id: number;
nama_lengkap: string;
};
export default function PinjamBarangModal({ barang, onClose, onSuccess }: PinjamModalProps) {
const [users, setUsers] = useState<UserType[]>([]);
const [userSerahId, setUserSerahId] = useState("");
const [userTerimaId, setUserTerimaId] = useState("");
const [submitting, setSubmitting] = useState(false);
// Ambil daftar user dari DB untuk opsi pilihan Penyerah & Penerima BAST
useEffect(() => {
fetch("http://localhost:5000/api/users")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
setUsers(resData.data);
}
})
.catch((err) => console.error("Gagal mengambil data user:", err));
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!userSerahId || !userTerimaId) {
alert("Pihak penyerah dan penerima wajib dipilih!");
return;
}
setSubmitting(true);
// Tembak data transaksi baru ke tabel BAST
fetch("http://localhost:5000/api/bast", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
barang_id: barang.barang_id,
user_serah_id: Number(userSerahId),
user_terima_id: Number(userTerimaId),
}),
})
.then((res) => res.json())
.then((data) => {
if (data.success) {
alert("Pengajuan pinjam barang (BAST) berhasil dibuat! Menunggu persetujuan kedua belah pihak.");
onSuccess();
} else {
alert("Gagal memproses pengajuan: " + data.message);
}
})
.catch((err) => console.error("Error submit BAST:", err))
.finally(() => setSubmitting(false));
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6 shadow-xl border">
<h2 className="text-xl font-bold text-gray-800 mb-4">Form Pengajuan Pinjam Barang</h2>
<div className="flex items-center gap-4 mb-4 p-3 bg-blue-50 rounded-xl">
<img src={barang.foto_barang} alt={barang.nama_barang} className="w-16 h-16 object-cover rounded-lg border bg-white" />
<div>
<h4 className="font-bold text-blue-900">{barang.nama_barang}</h4>
<p className="text-xs text-gray-500">Kode: {barang.kode_barang} | Stok: {barang.stok}</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Pihak Yang Menyerahkan Barang</label>
<select
value={userSerahId}
onChange={(e) => setUserSerahId(e.target.value)}
className="w-full p-2.5 border rounded-xl text-black bg-white focus:ring-2 focus:ring-blue-500"
required
>
<option value="">-- Pilih User Penyerah --</option>
{users.map((u) => (
<option key={u.user_id} value={u.user_id}>{u.nama_lengkap}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Pihak Yang Menerima/Meminjam Barang</label>
<select
value={userTerimaId}
onChange={(e) => setUserTerimaId(e.target.value)}
className="w-full p-2.5 border rounded-xl text-black bg-white focus:ring-2 focus:ring-blue-500"
required
>
<option value="">-- Pilih User Penerima --</option>
{users.map((u) => (
<option key={u.user_id} value={u.user_id}>{u.nama_lengkap}</option>
))}
</select>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 border rounded-xl text-gray-600 hover:bg-gray-100 transition"
disabled={submitting}
>
Batal
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 transition"
disabled={submitting}
>
{submitting ? "Memproses..." : "Ajukan Pinjaman"}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,118 @@
// src/components/sidebar/Sidebar.tsx
import { NavLink } from "react-router-dom";
import {
LayoutDashboard,
ClipboardList,
Settings,
Menu,
} from "lucide-react";
type SidebarProps = {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
};
const menuItems = [
{
name: "Dashboard",
path: "/userDashboard",
icon: <LayoutDashboard size={22} />,
},
{
name: "Barang",
path: "/userDashboard/master-data",
icon: <ClipboardList size={22} />,
},
{
name: "Transaksi",
path: "/userDashboard/transaksi",
icon: <ClipboardList size={22} />,
},
{
name: "Laporan",
path: "/userDashboard/laporan",
icon: <ClipboardList size={22} />,
},
{
name: "Settings",
path: "/userDashboard/settings",
icon: <Settings size={22} />,
},
];
export default function Sidebar({
isOpen,
setIsOpen,
}: SidebarProps) {
return (
<aside
className={`fixed top-0 left-0 h-screen bg-black text-white shadow-2xl transition-all duration-300 z-50
${isOpen ? "w-64" : "w-20"}`}
>
{/* HEADER */}
<div className="h-24 px-4 border-b border-gray-800 flex items-center justify-between">
{isOpen && (
<div>
<h1 className="text-3xl font-bold text-blue-500 leading-none">
SCHOOL
<br />
LOAN
</h1>
<p className="text-sm text-gray-400 mt-2">
Sistem Peminjaman
</p>
</div>
)}
{/* TOGGLE */}
<button
onClick={() => setIsOpen(!isOpen)}
className="bg-blue-600 hover:bg-blue-700 transition p-3 rounded-2xl"
>
<Menu size={22} />
</button>
</div>
{/* MENU */}
<nav className="mt-6 px-4 space-y-3">
{menuItems.map((item, index) => (
<NavLink
key={index}
to={item.path}
className={({ isActive }) =>
`flex items-center ${
isOpen ? "justify-start" : "justify-center"
} gap-4 px-5 py-4 rounded-2xl transition-all duration-200 ${
isActive
? "bg-blue-600 text-white shadow-lg"
: "text-gray-300 hover:bg-gray-900 hover:text-white"
}`
}
>
{item.icon}
{isOpen && (
<span className="text-lg font-medium">
{item.name}
</span>
)}
</NavLink>
))}
</nav>
{/* FOOTER */}
{isOpen && (
<div className="absolute bottom-5 left-5 text-sm text-gray-500">
© 2026 School Loan
</div>
)}
</aside>
);
}

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,31 @@
// src/layout/DashboardLayout.tsx
import { useState } from "react";
import { Outlet } from "react-router-dom";
import Sidebar from "../components/sidebar/userSidebar";
export default function DashboardLayout() {
const [isOpen, setIsOpen] =
useState(true);
return (
<div className="flex min-h-screen bg-[#f4f8ff]">
{/* SIDEBAR */}
<Sidebar
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
{/* MAIN CONTENT */}
<main
className={`flex-1 transition-all duration-300 ${
isOpen
? "ml-64"
: "ml-20"
}`}
>
<Outlet />
</main>
</div>
);
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

0
src/root.tsx Normal file
View File

228
src/routes/userBAST.tsx Normal file
View File

@ -0,0 +1,228 @@
// src/routes/BAST.tsx
import React, { useState, useEffect } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom"; // Kita pakai untuk redirect user biasa
import { FileText, ArrowRight, ClipboardList, User, ShieldAlert } from "lucide-react";
interface UserType {
user_id: number;
nama_lengkap: string;
role: string;
}
interface BarangType {
barang_id: number;
nama_barang: string;
}
export default function BAST() {
const [daftarUser, setDaftarUser] = useState<UserType[]>([]);
const [daftarBarang, setDaftarBarang] = useState<BarangType[]>([]);
// State Input Form
const [barangId, setBarangId] = useState("");
const [userSerahId, setUserSerahId] = useState(""); // Ditambahkan sesuai soal: Admin bebas pilih User Serah
const [userTerimaId, setUserTerimaId] = useState(""); // Ditambahkan sesuai soal: Admin bebas pilih User Terima
const [loadingSubmit, setLoadingSubmit] = useState(false);
// State Otentikasi Role
const [currentAdmin, setCurrentAdmin] = useState<UserType | null>(null);
const [isAuthorized, setIsAuthorized] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// 1. CEK SESSION LOGIN DAN ROLE NYA
const savedUser = sessionStorage.getItem("user");
if (!savedUser) {
alert("Silakan login terlebih dahulu!");
navigate("/login");
return;
}
try {
const parsed = JSON.parse(savedUser);
// KUNCI UTAMA SESUAI SOAL UKK: JIKA BUKAN ADMIN, BLOKIR AKSESNYA!
if (parsed.role !== "Admin") {
setIsAuthorized(false);
alert("Akses Ditolak! Berdasarkan soal UKK, hanya Admin yang boleh membuat dokumen BAST.");
navigate("/userDashboard"); // Buang user biasa ke dashboard mereka sendiri
return;
}
// Jika dia Admin, izinkan akses halaman ini
setCurrentAdmin(parsed);
setIsAuthorized(true);
} catch (e) {
console.error("Gagal membaca session:", e);
navigate("/login");
return;
}
// 2. AMBIL DATA PILIHAN USER (UNTUK DROPDOWN SERAH & TERIMA)
axios.get("http://localhost:5000/api/users")
.then((res) => {
if (res.data.success) setDaftarUser(res.data.data);
})
.catch((err) => console.error("Gagal memuat list user:", err));
// 3. AMBIL DATA BARANG DARI BACKEND
axios.get("http://localhost:5000/api/barang")
.then((res) => {
if (res.data.success) setDaftarBarang(res.data.data);
})
.catch((err) => console.error("Gagal memuat list barang:", err));
}, [navigate]);
// PROSES KIRIM FORM (ADMIN YANG MEMBUATKAN)
const handleSubmitBAST = async (e: React.FormEvent) => {
e.preventDefault();
if (!barangId || !userSerahId || !userTerimaId) {
alert("Harap lengkapi semua pilihan barang, pihak penyerah, dan pihak penerima!");
return;
}
if (userSerahId === userTerimaId) {
alert("Pihak Penyerah dan Pihak Penerima tidak boleh orang yang sama!");
return;
}
setLoadingSubmit(true);
try {
const response = await axios.post("http://localhost:5000/api/bast", {
barang_id: Number(barangId),
user_serah_id: Number(userSerahId), // Input dinamis pilihan Admin
user_terima_id: Number(userTerimaId), // Input dinamis pilihan Admin
});
if (response.data.success) {
alert("Dokumen BAST Baru Berhasil Diterbitkan! Status otomatis 'Menunggu Approval' dari kedua belah pihak.");
// Reset pilihan form
setBarangId("");
setUserSerahId("");
setUserTerimaId("");
} else {
alert("Gagal menerbitkan BAST: " + response.data.message);
}
} catch (error: any) {
console.error(error);
alert(error.response?.data?.message || "Terjadi kendala pada sistem backend.");
} finally {
setLoadingSubmit(false);
}
};
// Guarding UI biar tidak bocor sekilas saat loading validasi role
if (!isAuthorized) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900 text-white p-4">
<div className="text-center space-y-3">
<ShieldAlert className="mx-auto text-red-500 animate-pulse" size={48} />
<h2 className="text-xl font-bold">Memvalidasi Otoritas Akun...</h2>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-6 flex items-center justify-center">
<div className="max-w-2xl w-full bg-white border border-gray-200 rounded-3xl shadow-xl overflow-hidden">
{/* HEADER FORM (KHUSUS PORTAL ADMIN) */}
<div className="bg-gradient-to-r from-blue-700 to-indigo-800 p-6 text-white flex items-center gap-4">
<div className="p-3 bg-white/10 rounded-2xl">
<FileText size={28} />
</div>
<div>
<h1 className="text-2xl font-bold">Penerbitan BAST (Admin)</h1>
<p className="text-blue-100 text-xs mt-0.5">Petugas: {currentAdmin?.nama_lengkap} (Manajemen Inventaris)</p>
</div>
</div>
{/* BODY FORM */}
<form onSubmit={handleSubmitBAST} className="p-6 space-y-6">
{/* SELEKSI BARANG */}
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<ClipboardList size={16} className="text-gray-400" />
1. Pilih Inventaris Barang Sekolah
</label>
<select
value={barangId}
onChange={(e) => setBarangId(e.target.value)}
className="w-full bg-white border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-- Cari / Pilih Barang --</option>
{daftarBarang.map((b) => (
<option key={b.barang_id} value={b.barang_id}>
{b.nama_barang}
</option>
))}
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* SELEKSI PIHAK 1 (PENYERAH) */}
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<User size={16} className="text-green-600" />
2. Pihak 1 (User Penyerah)
</label>
<select
value={userSerahId}
onChange={(e) => setUserSerahId(e.target.value)}
className="w-full bg-white border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-- Pilih Penyerah --</option>
{daftarUser.map((u) => (
<option key={u.user_id} value={u.user_id}>
{u.nama_lengkap} ({u.role})
</option>
))}
</select>
</div>
{/* SELEKSI PIHAK 2 (PENERIMA) */}
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<User size={16} className="text-orange-500" />
3. Pihak 2 (User Penerima)
</label>
<select
value={userTerimaId}
onChange={(e) => setUserTerimaId(e.target.value)}
className="w-full bg-white border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-- Pilih Penerima --</option>
{daftarUser.map((u) => (
<option key={u.user_id} value={u.user_id}>
{u.nama_lengkap} ({u.role})
</option>
))}
</select>
</div>
</div>
{/* BUTTON PEMBUATAN DOKUMEN */}
<button
type="submit"
disabled={loadingSubmit}
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-300 text-white font-bold py-3 px-4 rounded-xl shadow-lg shadow-indigo-500/10 flex items-center justify-center gap-2 transition active:scale-[0.99]"
>
{loadingSubmit ? "Sedang Mendaftarkan Dokumen..." : (
<>
Terbitkan Dokumen BAST Resmi
<ArrowRight size={18} />
</>
)}
</button>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
import { useState } from "react";
import Hero from "../components/dashboard/userHero";
import About from "../components/dashboard/userAbout";
import Content from "../components/dashboard/userContent";
import Sidebar from "../components/sidebar/userSidebar";
export default function Dashboard() {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="flex bg-gray-100 min-h-screen">
{/* Sidebar */}
<Sidebar
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
{/* Content */}
<div
className={`transition-all duration-300 flex-1 p-6 ${
isOpen ? "ml-64" : "ml-20"
}`}
>
<Hero />
<About />
<Content />
</div>
</div>
);
}

210
src/routes/userLaporan.tsx Normal file
View File

@ -0,0 +1,210 @@
import { useState, useEffect } from "react";
import { Printer, FileSpreadsheet, RefreshCw } from "lucide-react";
// Definisikan tipe data sesuai skema database backend Express Abang
type TransaksiType = {
bast_id: number;
barang_id: number;
nama_barang: string;
foto_barang: string;
nama_kategori: string;
nama_lokasi: string;
user_serah_id: number;
nama_penyerah: string;
user_terima_id: number;
nama_penerima: string;
status_serah: string;
status_terima: string;
dibuat_pada: string;
};
export default function LaporanTransaksi() {
const [dataTransaksi, setDataTransaksi] = useState<TransaksiType[]>([]);
const [loading, setLoading] = useState(true);
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [currentUserRole, setCurrentUserRole] = useState<string>("User");
// Ambil ID & Role user secara aman dari sessionStorage/localStorage bawaan Login
const getFreshUserData = () => {
const savedUser = sessionStorage.getItem("user");
if (savedUser) {
try {
const parsed = JSON.parse(savedUser);
return {
id: parsed.user_id ? Number(parsed.user_id) : null,
role: parsed.role || "User"
};
} catch (e) {
console.error(e);
}
}
const localId = localStorage.getItem("user_id");
const localRole = localStorage.getItem("user_role");
return {
id: localId ? Number(localId) : null,
role: localRole || "User"
};
};
// Fungsi ambil data langsung dari API BAST backend Express
const fetchLaporan = () => {
setLoading(true);
fetch("http://localhost:5000/api/bast")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
setDataTransaksi(resData.data);
}
})
.catch((err) => console.error("Gagal mengambil data BAST untuk laporan:", err))
.finally(() => setLoading(false));
};
useEffect(() => {
const user = getFreshUserData();
setCurrentUserId(user.id);
setCurrentUserRole(user.role);
fetchLaporan();
}, []);
// --- FILTER LAPORAN SUPER KETAT (PINTU UTAMA SINKRONISASI USER) ---
const activeUserId = currentUserId || getFreshUserData().id;
const isAdmin = currentUserRole.toLowerCase() === "admin";
const laporanTersaring = dataTransaksi.filter((item) => {
// KONDISI 1: Hanya tampilkan berkas yang SUDAH BERHASIL TOTAL (Dua belah pihak setuju)
const isBerhasil = item.status_serah === "Approved" && item.status_terima === "Approved";
if (!isBerhasil) return false;
// KONDISI 2: Jika ADMIN -> Loloskan semua data berhasil untuk laporan rekap pimpinan
if (isAdmin) return true;
// KONDISI 3: Jika USER BISA (Nisa / Ujang) -> Hanya tampilkan yang melibatkan ID mereka
return Number(item.user_serah_id) === activeUserId || Number(item.user_terima_id) === activeUserId;
});
// Fungsi Tambahan: Cetak Laporan Mode PDF Browser (Nilai Plus UKK)
const handlePrint = () => {
window.print();
};
return (
<div className="min-h-screen bg-blue-100 p-6 print:bg-white print:p-0">
<div className="max-w-6xl mx-auto bg-white shadow-2xl rounded-2xl p-6 border border-blue-300 print:shadow-none print:border-none">
{/* HEADER LAPORAN & TOMBOL REKAP */}
<div className="flex flex-col sm:flex-row justify-between items-center mb-6 border-b border-blue-100 pb-4 gap-4 print:hidden">
<div className="text-center sm:text-left">
<h1 className="text-2xl font-bold text-blue-900">
{isAdmin ? "Laporan Rekap Transaksi Global" : "Laporan Transaksi Pribadi"}
</h1>
<p className="text-gray-500 text-xs mt-1">
{isAdmin
? "Menampilkan seluruh arsip sirkulasi BAST berstatus sukses"
: "Daftar riwayat serah terima barang inventaris sukses milik akun Anda"}
</p>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
<button
onClick={fetchLaporan}
className="flex items-center gap-1 bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-xl text-xs font-semibold border transition"
>
<RefreshCw size={14} /> Refresh
</button>
<button
onClick={handlePrint}
className="flex items-center gap-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-xl text-xs font-semibold shadow transition"
>
<Printer size={14} /> Cetak Laporan (PDF)
</button>
</div>
</div>
{/* HEADER KHUSUS SAAT DI-PRINT CETAK KERTAS */}
<div className="hidden print:block text-center mb-8 border-b-2 border-black pb-4">
<h1 className="text-2xl font-bold uppercase tracking-wide">PORTAL LOGISTIK INVENTARIS BAST</h1>
<p className="text-sm font-medium mt-1">Dokumen Resmi Bukti Riwayat Serah Terima Barang Sukses - UKK 2026</p>
<p className="text-xs text-gray-500 mt-1">Dicetak oleh: ID-{activeUserId} ({currentUserRole.toUpperCase()}) pada: {new Date().toLocaleDateString("id-ID")}</p>
</div>
{/* TABEL DATA REAL UTK LAPORAN */}
<div className="overflow-x-auto rounded-xl shadow-inner bg-gray-50">
<table className="w-full border border-blue-300">
<thead className="bg-blue-700 text-white print:bg-gray-200 print:text-black">
<tr>
<th className="p-3 border border-blue-300 text-sm">No</th>
<th className="p-3 border border-blue-300 text-sm">Kode BAST</th>
<th className="p-3 border border-blue-300 text-sm">Pengirim / Penyerah</th>
<th className="p-3 border border-blue-300 text-sm">Penerima</th>
<th className="p-3 border border-blue-300 text-sm">Nama Barang</th>
<th className="p-3 border border-blue-300 text-sm">Lokasi Sektor</th>
<th className="p-3 border border-blue-300 text-sm">Tanggal Disetujui</th>
<th className="p-3 border border-blue-300 text-sm">Status Dokumen</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={8} className="p-8 text-center text-gray-500 font-medium bg-white">
Sedangkan memvalidasi data laporan Anda...
</td>
</tr>
) : laporanTersaring.length === 0 ? (
<tr>
<td colSpan={8} className="p-12 text-center text-gray-400 font-medium bg-white border border-dashed">
Belum ada riwayat transaksi berstatus "Berhasil" (Approved Bersama) untuk ID Akun: {activeUserId}.
</td>
</tr>
) : (
laporanTersaring.map((item, index) => (
<tr
key={item.bast_id}
className="text-center bg-white hover:bg-blue-50/50 transition odd:bg-gray-50/30 print:bg-white text-gray-800 text-xs"
>
<td className="p-3 border border-blue-200 font-medium">
{index + 1}
</td>
<td className="p-3 border border-blue-200 font-bold text-blue-600 print:text-black">
BAST-00{item.bast_id}
</td>
<td className="p-3 border border-blue-200 font-medium">
{item.nama_penyerah} {Number(item.user_serah_id) === activeUserId && <span className="text-[10px] text-blue-500 print:hidden">(Anda)</span>}
</td>
<td className="p-3 border border-blue-200 font-medium">
{item.nama_penerima} {Number(item.user_terima_id) === activeUserId && <span className="text-[10px] text-blue-500 print:hidden">(Anda)</span>}
</td>
<td className="p-3 border border-blue-200 font-bold text-gray-900">
{item.nama_barang}
</td>
<td className="p-3 border border-blue-200 text-gray-600">
{item.nama_lokasi}
</td>
<td className="p-3 border border-blue-200">
{item.dibuat_pada ? new Date(item.dibuat_pada).toLocaleDateString("id-ID") : "-"}
</td>
<td className="p-3 border border-blue-200 text-green-600 font-black tracking-wide uppercase text-[11px]">
BERHASIL
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<button>
<FileSpreadsheet size={18} />
Export Excel
</button>
{/* FOOTER NOTIFIKASI */}
<div className="mt-4 text-right text-xs text-gray-400 print:hidden">
*Data di atas ditarik langsung secara sinkron dari sistem server lokal BAST.
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,160 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router';
import { Lock, User, ShieldAlert, Eye, EyeOff } from 'lucide-react';
const LoginKasir = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
// Bersihkan sisa login lama biar fresh
useEffect(() => {
localStorage.clear();
sessionStorage.clear();
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
// Tembak API Login ke Backend Express
const response = await axios.post('http://localhost:5000/api/login', {
username: username,
password: password
});
const dataRespon = response?.data?.data || response?.data || {};
const tokenRespon = response?.data?.token || dataRespon?.token || "dummy-token";
const userRole = dataRespon?.role || dataRespon?.user_role || "User";
const namaUser = dataRespon?.nama_lengkap || dataRespon?.nama || username || "User";
// 1. SIMPAN KE LOCAL STORAGE & SESSION STORAGE
localStorage.setItem('token', tokenRespon);
localStorage.setItem('user_role', userRole);
localStorage.setItem('user_name', namaUser);
localStorage.setItem('user_id', String(dataRespon?.user_id || dataRespon?.id || "1"));
const payloadUserUntukSession = {
user_id: String(dataRespon?.user_id || dataRespon?.id || "1"),
username: username,
role: userRole,
nama_lengkap: namaUser,
email: dataRespon?.email || `${username}@gmail.com`,
no_telp: dataRespon?.no_telp || "081234567890",
alamat: dataRespon?.alamat || "Jl. Merdeka No.123"
};
sessionStorage.setItem('user', JSON.stringify(payloadUserUntukSession));
alert(`Selamat Datang, ${namaUser}! 🎉`);
// --- 2. AMANKAN REDIRECT (ANTISIPASI JIKA /DASHBOARD TIDAK ADA) ---
try {
navigate('/UserDashboard');
} catch (routingErr) {
// Jika rute /dashboard crash/tidak ada, paksa pindah ke halaman transaksi atau root
console.warn("Rute /dashboard tidak ditemukan, dialihkan ke /transaksi");
navigate('/transaksi');
}
} catch (err: any) {
console.error("Detail Error Login:", err);
setError(err.response?.data?.message || err.message || 'Gagal terhubung ke server');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-[#1A1D21] flex items-center justify-center p-4">
<div className="max-w-md w-full bg-[#24272C] rounded-2xl p-8 shadow-2xl border border-white/5">
{/* Header */}
<div className="text-center mb-8">
<div className="bg-blue-500/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="text-blue-500" size={32} />
</div>
<h2 className="text-2xl font-bold text-white">Portal BAST</h2>
<p className="text-gray-400 text-sm mt-2">Silahkan masuk ke akun anda</p>
</div>
{/* Notif Error */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-3 rounded-lg mb-6 flex items-center gap-3 text-sm">
<ShieldAlert size={18} />
{error}
</div>
)}
<form onSubmit={handleLogin} className="space-y-5">
{/* Input Username */}
<div>
<label className="block text-gray-300 text-sm font-medium mb-2">Username</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500">
<User size={18} />
</div>
<input
type="text"
placeholder="Masukkan username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full bg-[#1A1D21] border border-white/10 rounded-xl pl-10 pr-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 text-sm transition-all"
required
/>
</div>
</div>
{/* Input Password */}
<div>
<label className="block text-gray-300 text-sm font-medium mb-2">Password</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500">
<Lock size={18} />
</div>
<input
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-[#1A1D21] border border-white/10 rounded-xl pl-10 pr-10 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 text-sm transition-all"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{/* Tombol Submit */}
<button
type="submit"
disabled={loading}
className="w-full mt-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold py-3 rounded-xl shadow-lg shadow-blue-500/20 transition-all transform active:scale-[0.98]"
>
{loading ? 'Menghubungkan...' : 'Masuk Sekarang'}
</button>
</form>
<p className="text-center text-gray-500 text-xs mt-8">
&copy; 2026 WinterAccess - Login Kasir
</p>
</div>
</div>
);
};
export default LoginKasir;

View File

@ -0,0 +1,21 @@
// src/routes/ManajemenUsers.tsx
// COMPONENTS
import UsersHero from "../components/manajemenUsers/UserHero";
import UsersFilter from "../components/manajemenUsers/UserFilter";
import UsersTable from "../components/manajemenUsers/UserTable";
export default function ManajemenUsers() {
return (
<div className="min-h-screen bg-[#f4f8ff] p-6">
{/* HERO */}
<UsersHero />
{/* FILTER */}
<UsersFilter />
{/* TABLE */}
<UsersTable />
</div>
);
}

View File

@ -0,0 +1,111 @@
// src/routes/MasterData.tsx
import { useState, useEffect } from "react";
// COMPONENTS
import BarangHero from "../components/masterData/userBarangHero";
import BarangFilter from "../components/masterData/userBarangFilter";
import BarangGrid from "../components/masterData/userBarangGrid";
import PinjamBarangModal from "../components/masterData/userPinjamBarangModal";
export type BarangType = {
barang_id: number;
kode_barang: string;
nama_barang: string;
foto_barang: string;
status_barang: string;
stok: number;
nama_kategori: string;
nama_lokasi: string;
qr_image?: string;
};
export default function MasterData() {
const [selectedBarang, setSelectedBarang] = useState<BarangType | null>(null);
const [barangList, setBarangList] = useState<BarangType[]>([]);
const [loading, setLoading] = useState(true);
// STATE BARU UNTUK SEARCH DAN FILTER MULTIPLE
const [selectedKategori, setSelectedKategori] = useState("Semua");
const [searchQuery, setSearchQuery] = useState("");
const [selectedStatus, setSelectedStatus] = useState("Semua");
const fetchBarang = () => {
setLoading(true);
fetch("http://localhost:5000/api/barang")
.then((res) => res.json())
.then((data) => {
if (data.success && Array.isArray(data.data)) {
setBarangList(data.data);
}
})
.catch((err) => console.error("Gagal memuat data barang:", err))
.finally(() => setLoading(false));
};
useEffect(() => {
fetchBarang();
}, []);
// PROSES FILTER GABUNGAN (KATEGORI + SEARCH NAMA/KODE + STATUS KONDISI)
const filteredBarang = barangList.filter((barang) => {
// 1. Filter Kategori
const matchKategori =
selectedKategori === "Semua" || barang.nama_kategori === selectedKategori;
// 2. Filter Status Kondisi
const matchStatus =
selectedStatus === "Semua" || barang.status_barang === selectedStatus;
// 3. Filter Search (Bisa cari berdasarkan Nama Barang ATAU Kode Barang)
const matchesSearch =
barang.nama_barang.toLowerCase().includes(searchQuery.toLowerCase()) ||
barang.kode_barang.toLowerCase().includes(searchQuery.toLowerCase());
// Barang harus lolos ketiga filter di atas
return matchKategori && matchStatus && matchesSearch;
});
return (
<div className="min-h-screen bg-[#f4f8ff] p-6">
{/* HERO */}
<BarangHero />
{/* FILTER SEKARANG MENAMPUNG EMPOWMENT STATE BARU */}
<BarangFilter
selectedKategori={selectedKategori}
setSelectedKategori={setSelectedKategori}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
/>
{/* GRID DAFTAR BARANG */}
{loading ? (
<div className="text-center py-10 text-gray-500 font-medium">
Sedang memuat data inventaris dari database...
</div>
) : filteredBarang.length === 0 ? (
<div className="text-center py-10 text-gray-400 font-medium bg-white rounded-2xl border border-dashed">
Barang yang Abang cari tidak ditemukan.
</div>
) : (
<BarangGrid barangList={filteredBarang} onPinjam={setSelectedBarang} />
)}
{/* MODAL FORM PENGAJUAN PINJAM */}
{selectedBarang && (
<PinjamBarangModal
barang={selectedBarang}
onClose={() => setSelectedBarang(null)}
onSuccess={() => {
fetchBarang();
setSelectedBarang(null);
}}
/>
)}
</div>
);
}

248
src/routes/userSettings.tsx Normal file
View File

@ -0,0 +1,248 @@
import {
Lock,
Info,
LogOut,
Moon,
Sun,
X,
} from "lucide-react";
import {
useEffect,
useState,
} from "react";
export default function SettingsPage() {
// Secara default kita set false (berarti aplikasi mulai dengan Light Mode)
const [darkMode, setDarkMode] = useState(false);
const [showAbout, setShowAbout] = useState(false);
// Cek jika sebelumnya pengguna pernah sengaja menyimpan pilihan "dark"
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
setDarkMode(true);
document.documentElement.classList.add("dark");
} else {
setDarkMode(false);
document.documentElement.classList.remove("dark");
}
}, []);
// Fungsi saklar untuk mengubah tema secara instan
const toggleTheme = () => {
if (darkMode) {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
setDarkMode(false);
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
setDarkMode(true);
}
};
const settingsMenu = [
{
title: "Ganti Password",
desc: "Ubah password akun",
icon: <Lock size={26} />,
},
{
title: "Tentang Aplikasi",
desc: "Informasi aplikasi School Loan",
icon: <Info size={26} />,
action: () => setShowAbout(true),
},
];
return (
/* Jaminan Light Mode:
Jika `darkMode` bernilai false, komponen ini akan dipaksa menggunakan warna
latar terang (`bg-gray-100`) dan teks gelap (`text-gray-800`).
*/
<div className={`p-8 min-h-screen transition-all duration-300 ${darkMode ? "dark bg-gray-900 text-white" : "bg-gray-100 text-gray-800"}`}>
{/* HEADER */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className={`text-5xl font-bold ${darkMode ? "text-white" : "text-gray-800"}`}>
Settings
</h1>
<p className={`mt-2 ${darkMode ? "text-gray-300" : "text-gray-500"}`}>
Pengaturan aplikasi BAST
</p>
</div>
{/* TOMBOL PENGUBAH TEMA */}
<button
onClick={toggleTheme}
type="button"
className="
flex
items-center
gap-3
bg-blue-600
hover:bg-blue-700
text-white
px-5
py-3
rounded-2xl
shadow-lg
transition-all
cursor-pointer
font-medium
"
>
{darkMode ? <Sun size={22} /> : <Moon size={22} />}
{darkMode ? "Light Mode" : "Dark Mode"}
</button>
</div>
{/* MENU */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{settingsMenu.map((item, index) => (
<div
key={index}
onClick={item.action}
className={`
rounded-3xl
p-6
shadow-md
hover:shadow-xl
transition-all
duration-300
cursor-pointer
${darkMode ? "bg-gray-800 border border-gray-700" : "bg-white border border-transparent"}
`}
>
<div className="flex items-center justify-between">
<div>
<h2 className={`text-xl font-semibold ${darkMode ? "text-white" : "text-gray-800"}`}>
{item.title}
</h2>
<p className={`text-sm mt-2 ${darkMode ? "text-gray-300" : "text-gray-500"}`}>
{item.desc}
</p>
</div>
<div className={`
w-14
h-14
rounded-2xl
flex
items-center
justify-center
${darkMode ? "bg-blue-900 text-blue-300" : "bg-blue-100 text-blue-600"}
`}>
{item.icon}
</div>
</div>
</div>
))}
</div>
{/* LOGOUT */}
<div className="mt-10">
<div
onClick={() => {
localStorage.clear();
window.location.href = "/";
}}
className="
bg-red-500
hover:bg-red-600
text-white
rounded-3xl
p-6
shadow-lg
cursor-pointer
transition-all
duration-300
w-full
md:w-[320px]
"
>
<div className="flex items-center gap-4">
<div className="
w-14
h-14
rounded-2xl
bg-white/20
flex
items-center
justify-center
">
<LogOut size={26} />
</div>
<div>
<h2 className="text-2xl font-bold">Logout</h2>
<p className="text-red-100 text-sm mt-1">
Keluar dari aplikasi
</p>
</div>
</div>
</div>
</div>
{/* MODAL ABOUT */}
{showAbout && (
<div className="
fixed
inset-0
bg-black/50
backdrop-blur-sm
flex
items-center
justify-center
z-50
">
<div className={`
w-[90%]
md:w-[600px]
rounded-3xl
p-8
shadow-2xl
relative
${darkMode ? "bg-gray-800 text-white border border-gray-700" : "bg-white text-gray-800"}
`}>
{/* CLOSE */}
<button
onClick={() => setShowAbout(false)}
className="
absolute
top-5
right-5
text-gray-400
hover:text-red-500
transition-colors
"
>
<X size={24} />
</button>
{/* TITLE */}
<h1 className={`text-3xl font-bold mb-4 ${darkMode ? "text-blue-400" : "text-blue-600"}`}>
Tentang Aplikasi
</h1>
{/* CONTENT */}
<p className={`leading-8 ${darkMode ? "text-gray-300" : "text-gray-700"}`}>
<span className="font-bold">School Loan</span> adalah aplikasi peminjaman dan pengelolaan barang sekolah berbasis web yang digunakan untuk membantu proses transaksi BAST (Berita Acara Serah Terima), pengelolaan data barang, monitoring stok barang, serta pembuatan laporan secara cepat, akurat, dan efisien.
<br /><br />
Aplikasi ini dibuat untuk mempermudah petugas sekolah dalam mengelola data peminjaman barang seperti laptop, buku, proyektor, alat tulis, dan perlengkapan lainnya secara modern dan terintegrasi.
<br /><br />
<span className="font-semibold">Versi:</span> 1.0
<br />
<span className="font-semibold">Developer:</span> Akbar
<br />
<span className="font-semibold">Framework:</span> React JS, Express JS, MySQL
</p>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,220 @@
import {
FileText,
Search,
Eye,
Download,
Package,
Clock,
CheckCircle,
ShoppingBag,
} from "lucide-react";
export default function TransaksiBASTUsers() {
const transaksi = [
{
no: "BAST-001",
tanggal: "15 Mei 2026",
barang: "Laptop ASUS ROG",
status: "Dipinjam",
},
{
no: "BAST-002",
tanggal: "16 Mei 2026",
barang: "Mouse Logitech",
status: "Dibeli",
},
{
no: "BAST-003",
tanggal: "17 Mei 2026",
barang: "Monitor Samsung",
status: "Dipinjam",
},
];
return (
<div className="min-h-screen bg-gray-100 p-6">
{/* HEADER */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-4xl font-bold flex items-center gap-3">
<FileText className="text-blue-600" size={35} />
Transaksi BAST Users
</h1>
<p className="text-gray-500 mt-2">
Riwayat transaksi barang users
</p>
</div>
<button className="bg-blue-600 text-white px-5 py-3 rounded-2xl font-semibold">
+ Tambah Transaksi
</button>
</div>
{/* CARD */}
<div className="grid grid-cols-4 gap-5 mb-8">
<div className="bg-white rounded-3xl p-5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400">
Total Transaksi
</p>
<h2 className="text-3xl font-bold mt-2">
120
</h2>
</div>
<Package className="text-blue-600" size={40} />
</div>
</div>
<div className="bg-white rounded-3xl p-5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400">
Dipinjam
</p>
<h2 className="text-3xl font-bold mt-2">
85
</h2>
</div>
<Clock className="text-yellow-500" size={40} />
</div>
</div>
<div className="bg-white rounded-3xl p-5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400">
Dibeli
</p>
<h2 className="text-3xl font-bold mt-2">
35
</h2>
</div>
<ShoppingBag className="text-green-600" size={40} />
</div>
</div>
<div className="bg-white rounded-3xl p-5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400">
Selesai
</p>
<h2 className="text-3xl font-bold mt-2">
118
</h2>
</div>
<CheckCircle className="text-purple-600" size={40} />
</div>
</div>
</div>
{/* TABLE */}
<div className="bg-white rounded-3xl p-6 shadow-sm">
{/* TOP */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">
Data Transaksi
</h2>
<div className="relative">
<Search
className="absolute left-4 top-4 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Cari transaksi..."
className="border pl-11 pr-4 py-3 rounded-2xl outline-none"
/>
</div>
</div>
{/* TABLE */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-gray-500">
<th className="pb-4">No BAST</th>
<th className="pb-4">Tanggal</th>
<th className="pb-4">Barang</th>
<th className="pb-4">Status</th>
<th className="pb-4">Aksi</th>
</tr>
</thead>
<tbody>
{transaksi.map((item, index) => (
<tr
key={index}
className="border-b hover:bg-gray-50"
>
<td className="py-5 font-semibold">
{item.no}
</td>
<td>{item.tanggal}</td>
<td>{item.barang}</td>
<td>
<span
className={`px-4 py-2 rounded-xl text-sm font-semibold ${
item.status === "Dipinjam"
? "bg-yellow-100 text-yellow-700"
: "bg-green-100 text-green-700"
}`}
>
{item.status}
</span>
</td>
<td>
<div className="flex gap-3">
<button className="bg-blue-100 text-blue-600 p-3 rounded-xl">
<Eye size={18} />
</button>
<button className="bg-green-100 text-green-600 p-3 rounded-xl">
<Download size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,235 @@
import { useState, useEffect } from "react";
import { Check, X, MapPin, User, UserCheck, Tag, Shield } from "lucide-react";
type TransaksiType = {
bast_id: number;
barang_id: number;
nama_barang: string;
foto_barang: string;
nama_kategori: string;
nama_lokasi: string;
user_serah_id: number;
nama_penyerah: string;
user_terima_id: number;
nama_penerima: string;
status_serah: string;
status_terima: string;
dibuat_pada: string;
};
export default function Transaksi() {
const [dataTransaksi, setDataTransaksi] = useState<TransaksiType[]>([]);
const [loading, setLoading] = useState(true);
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [currentUserRole, setCurrentUserRole] = useState<string>("User"); // <-- Tambahan State Role
// Ambil data user secara lengkap dan aman dari sessionStorage
const getFreshUserData = () => {
const savedUser = sessionStorage.getItem("user");
if (savedUser) {
try {
const parsed = JSON.parse(savedUser);
return {
id: parsed.user_id ? Number(parsed.user_id) : null,
role: parsed.role || "User"
};
} catch (e) {
console.error("Gagal parse user session:", e);
}
}
// Backup jika di session kosong, intip dari localStorage bawaan login
const localId = localStorage.getItem("user_id");
const localRole = localStorage.getItem("user_role");
return {
id: localId ? Number(localId) : null,
role: localRole || "User"
};
};
const fetchTransaksi = () => {
setLoading(true);
fetch("http://localhost:5000/api/bast")
.then((res) => res.json())
.then((resData) => {
if (resData.success && Array.isArray(resData.data)) {
setDataTransaksi(resData.data);
}
})
.catch((err) => console.error("Gagal mengambil data BAST:", err))
.finally(() => setLoading(false));
};
useEffect(() => {
const user = getFreshUserData();
setCurrentUserId(user.id);
setCurrentUserRole(user.role);
fetchTransaksi();
const handleStorageChange = () => {
const updatedUser = getFreshUserData();
setCurrentUserId(updatedUser.id);
setCurrentUserRole(updatedUser.role);
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
const handleAksiPersetujuan = (item: TransaksiType, aksi: "Approved" | "Rejected") => {
const user = getFreshUserData();
const activeId = user.id || currentUserId;
if (!activeId) {
alert("Sesi login habis atau tidak valid. Silakan login kembali.");
return;
}
let tipeStatusdiUpdate = "";
if (Number(item.user_serah_id) === activeId) {
tipeStatusdiUpdate = "serah";
} else if (Number(item.user_terima_id) === activeId) {
tipeStatusdiUpdate = "terima";
}
if (!tipeStatusdiUpdate) {
alert("Anda tidak memiliki otoritas untuk menyetujui transaksi milik orang lain!");
return;
}
fetch(`http://localhost:5000/api/bast/${item.bast_id}/status`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
peran: tipeStatusdiUpdate,
status: aksi,
}),
})
.then((res) => res.json())
.then((data) => {
if (data.success) {
alert(`Berhasil mengirim respon ${aksi}!`);
fetchTransaksi();
} else {
alert("Gagal mengupdate status: " + data.message);
}
})
.catch((err) => console.error(err));
};
// --- LOGIKA FILTER PINTAR (MONITORING ADMIN VS KOTAK MASUK PRIVAT USER) ---
const activeUserIdForFilter = currentUserId || getFreshUserData().id;
const isAdmin = currentUserRole.toLowerCase() === "admin";
const transaksiTersaring = dataTransaksi.filter((item) => {
// 1. JIKA ADMIN -> Loloskan semua data untuk fungsi monitoring / pemantauan
if (isAdmin) return true;
// 2. JIKA KASIR / USER (NISA & UJANG) -> Hanya loloskan data miliknya sendiri
if (!activeUserIdForFilter) return false;
return Number(item.user_serah_id) === activeUserIdForFilter || Number(item.user_terima_id) === activeUserIdForFilter;
});
return (
<div className="min-h-screen bg-gray-100 p-6">
{/* HEADER SECTION */}
<div className="mb-6 flex justify-between items-center bg-white p-4 rounded-xl border shadow-sm">
<div>
<h1 className="text-2xl font-bold text-gray-950 flex items-center gap-2">
{isAdmin && <Shield className="text-red-500" size={24} />}
{isAdmin ? "Monitoring Transaksi BAST (Mode Admin)" : "Kotak Masuk Transaksi"}
</h1>
<p className="text-gray-500 text-xs mt-1">
{isAdmin
? "Memantau seluruh status sirkulasi dokumen persetujuan inventaris secara real-time"
: "Persetujuan dokumen Berita Acara Serah Terima (BAST) akun Anda"}
</p>
</div>
<div className="text-right flex flex-col sm:flex-row gap-2">
<span className={`text-xs px-3 py-1.5 rounded-lg font-semibold border ${isAdmin ? "bg-red-50 text-red-700 border-red-200" : "bg-indigo-50 text-indigo-700 border-indigo-200"}`}>
Role: {currentUserRole.toUpperCase()}
</span>
<span className="text-xs bg-gray-50 text-gray-700 px-3 py-1.5 rounded-lg font-semibold border border-gray-200">
ID Akun: {activeUserIdForFilter || "Belum Login"}
</span>
</div>
</div>
{/* BODY DATA LIST */}
<div className="space-y-4">
{loading ? (
<div className="text-center py-8 text-gray-500 font-medium bg-white rounded-xl border">Memuat sirkulasi berkas...</div>
) : transaksiTersaring.length === 0 ? (
<div className="text-center py-12 text-gray-400 font-medium bg-white rounded-xl border border-dashed">
{isAdmin
? "Belum ada data transaksi BAST yang dibuat di database backend."
: `Tidak ada dokumen BAST masuk yang memerlukan konfirmasi dari akun Anda (ID: ${activeUserIdForFilter}).`}
</div>
) : (
transaksiTersaring.map((item) => {
const amIPenyerah = Number(item.user_serah_id) === activeUserIdForFilter;
const amIPenerima = Number(item.user_terima_id) === activeUserIdForFilter;
let statusSaya = "Menunggu";
if (amIPenyerah) statusSaya = item.status_serah;
if (amIPenerima) statusSaya = item.status_terima;
return (
<div key={item.bast_id} className="bg-white border rounded-xl shadow-sm p-4 flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<div className="flex items-center gap-4 w-full lg:w-auto">
<img src={item.foto_barang || "https://via.placeholder.com/150"} alt={item.nama_barang} className="w-20 h-20 object-cover rounded-xl border bg-gray-50" />
<div>
<div className="flex items-center gap-2">
<h2 className="text-lg font-bold text-gray-800">{item.nama_barang}</h2>
<span className="text-[10px] bg-blue-50 text-blue-700 px-2 py-0.5 rounded font-semibold">BAST-00{item.bast_id}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1 mt-2 text-xs text-gray-600">
<div className="flex items-center gap-1"><Tag size={12} /><span>Kategori: <strong>{item.nama_kategori}</strong></span></div>
<div className="flex items-center gap-1"><MapPin size={12} /><span>Lokasi: <strong>{item.nama_lokasi}</strong></span></div>
<div className="flex items-center gap-1">
<UserCheck size={12} className="text-green-600" />
<span>Penyerah: <strong className={amIPenyerah && !isAdmin ? "text-blue-600" : ""}>{item.nama_penyerah} {amIPenyerah && !isAdmin && "(Anda)"}</strong>
<span className={`ml-1 text-[9px] px-1.5 py-0.2 rounded font-bold ${item.status_serah === "Approved" ? "bg-green-100 text-green-700" : item.status_serah === "Rejected" ? "bg-red-100 text-red-700" : "bg-yellow-100 text-yellow-700"}`}>{item.status_serah}</span>
</span>
</div>
<div className="flex items-center gap-1">
<User size={12} className="text-orange-500" />
<span>Penerima: <strong className={amIPenerima && !isAdmin ? "text-blue-600" : ""}>{item.nama_penerima} {amIPenerima && !isAdmin && "(Anda)"}</strong>
<span className={`ml-1 text-[9px] px-1.5 py-0.2 rounded font-bold ${item.status_terima === "Approved" ? "bg-green-100 text-green-700" : item.status_terima === "Rejected" ? "bg-red-100 text-red-700" : "bg-yellow-100 text-yellow-700"}`}>{item.status_terima}</span>
</span>
</div>
</div>
</div>
</div>
{/* AREA TOMBOL AKSI / KETERANGAN MONITORING */}
<div className="flex items-center gap-2 w-full lg:w-auto justify-end border-t lg:border-t-0 pt-2 lg:pt-0">
{isAdmin ? (
// TAMPILAN KHUSUS ADMIN (MONITORING STATUS KEDUA BELAH PIHAK)
<div className="text-right">
<span className="text-[11px] bg-gray-100 border text-gray-600 px-3 py-1 rounded-md font-medium">
Log Terpantau Admin
</span>
</div>
) : (
// TAMPILAN UNTUK USER BIASA (NISA / UJANG)
statusSaya === "Menunggu" || statusSaya === "Waiting" ? (
<>
<button onClick={() => handleAksiPersetujuan(item, "Approved")} className="flex items-center gap-1 bg-green-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold shadow-sm hover:bg-green-700 transition"><Check size={14} /> Setuju</button>
<button onClick={() => handleAksiPersetujuan(item, "Rejected")} className="flex items-center gap-1 bg-red-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold shadow-sm hover:bg-red-700 transition"><X size={14} /> Tolak</button>
</>
) : (
<div className="text-right">
{statusSaya === "Approved" ? <span className="text-green-600 text-xs font-bold">✓ Anda Menyetujui</span> : <span className="text-red-600 text-xs font-bold">✕ Anda Menolak</span>}
</div>
)
)}
</div>
</div>
);
})
)}
</div>
</div>
);
}

7
tailwind.config.js Normal file
View File

@ -0,0 +1,7 @@
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}

25
tsconfig.app.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})