first commit
|
|
@ -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?
|
||||
|
|
@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 757 KiB |
|
After Width: | Height: | Size: 750 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 821 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
© 2026 WinterAccess - Login Kasir
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginKasir;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||