From df426001985fc6f10418974ad8a3b655654380a4 Mon Sep 17 00:00:00 2001 From: Tassana Pralao <65160024@go.buu.ac.th> Date: Sat, 22 Mar 2025 22:22:37 +0700 Subject: [PATCH] update --- client/package-lock.json | 63 +++++++++++++ client/package.json | 1 + client/src/App.css | 33 ++++++- client/src/api/category.jsx | 5 +- client/src/api/equipment.jsx | 14 +-- .../components/admin/FormEditEquipment.jsx | 2 +- client/src/components/admin/FormEquipment.jsx | 50 ++++++---- client/src/components/admin/SidebarAdmin.jsx | 21 +---- client/src/components/admin/Uploadfile.jsx | 11 ++- client/src/components/card/EquipmentCard.jsx | 47 ++++++++++ client/src/components/card/SearchCard.jsx | 94 +++++++++++++++++++ client/src/index.css | 3 - client/src/layouts/Layout.jsx | 2 +- client/src/pages/All.jsx | 47 +++++++++- client/src/stock/borrow-stock.jsx | 72 +++++++------- server/routes/category.js | 2 +- server/server.js | 8 ++ 17 files changed, 381 insertions(+), 94 deletions(-) create mode 100644 client/src/components/card/EquipmentCard.jsx create mode 100644 client/src/components/card/SearchCard.jsx diff --git a/client/package-lock.json b/client/package-lock.json index 7cc3d6a3..827cb54e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "bootstrap": "^5.3.3", "cors": "^2.8.5", "lucide-react": "^0.483.0", + "rc-slider": "^11.1.8", "react": "^19.0.0", "react-dom": "^19.0.0", "react-image-file-resizer": "^0.4.8", @@ -285,6 +286,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -1869,6 +1882,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3388,6 +3407,38 @@ "node": ">=6" } }, + "node_modules/rc-slider": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -3415,6 +3466,12 @@ "integrity": "sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ==", "license": "MIT" }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3478,6 +3535,12 @@ "react-dom": "^18 || ^19" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/client/package.json b/client/package.json index 7eb5f542..16941ba3 100644 --- a/client/package.json +++ b/client/package.json @@ -17,6 +17,7 @@ "bootstrap": "^5.3.3", "cors": "^2.8.5", "lucide-react": "^0.483.0", + "rc-slider": "^11.1.8", "react": "^19.0.0", "react-dom": "^19.0.0", "react-image-file-resizer": "^0.4.8", diff --git a/client/src/App.css b/client/src/App.css index 83f10b7d..3b9ed328 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -41,4 +41,35 @@ color: #888; } -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +.edit-btn:hover { + background-color: #ffcc00 !important; + border-color: #e6b800 !important; + transform: scale(1.1); + transition: 0.3s ease-in-out; +} + +.delete-btn:hover { + background-color: #ff4d4d !important; + border-color: #cc0000 !important; + transform: scale(1.1); + transition: 0.3s ease-in-out; +} + +.sidebar-link { + color: #ffffff !important; + padding: 10px 15px; + border-radius: 8px; + transition: background-color 0.3s ease-in-out, transform 0.2s ease-in-out; +} + +.sidebar-link:hover { + background-color: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.sidebar-link.active { + background-color: rgba(255, 255, 255, 0.3); + font-weight: bold; +} diff --git a/client/src/api/category.jsx b/client/src/api/category.jsx index f58b25db..1dddb7db 100644 --- a/client/src/api/category.jsx +++ b/client/src/api/category.jsx @@ -8,11 +8,8 @@ export const createCategory = async (token, form) => { }) } -export const listCategory = async (token) => { +export const listCategory = async () => { return axios.get('http://localhost:3000/api/category',{ - headers:{ - Authorization: `Bearer ${token}` - } }) } diff --git a/client/src/api/equipment.jsx b/client/src/api/equipment.jsx index 5875b648..7373dd6a 100644 --- a/client/src/api/equipment.jsx +++ b/client/src/api/equipment.jsx @@ -8,13 +8,9 @@ export const createEquipment = async (token, form) => { }); }; -export const listEquipment = async (token, count = 20) => { - return axios.get('http://localhost:3000/api/equipments/'+count, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); -}; +export const listEquipment = async (count = 20) => { + return axios.get('http://localhost:3000/api/equipments/'+count +)}; export const readEquipment = async (token, id) => { return axios.get('http://localhost:3000/api/equipment/'+id, { @@ -60,4 +56,8 @@ export const removeFiles = async (token, public_id) => { Authorization: `Bearer ${token}`, }, }); +}; + +export const searchFilters = async (arg) => { + return axios.post("http://localhost:3000/api/search/filters",arg); }; \ No newline at end of file diff --git a/client/src/components/admin/FormEditEquipment.jsx b/client/src/components/admin/FormEditEquipment.jsx index b6d740af..08d77545 100644 --- a/client/src/components/admin/FormEditEquipment.jsx +++ b/client/src/components/admin/FormEditEquipment.jsx @@ -28,7 +28,7 @@ const FormEditEquipment = () => { useEffect(() => { getCategory(); - fetchEquipment(token, id); + fetchEquipment(token, id, form); }, []); const fetchEquipment = async (token, id) => { diff --git a/client/src/components/admin/FormEquipment.jsx b/client/src/components/admin/FormEquipment.jsx index a069ffec..11e382b5 100644 --- a/client/src/components/admin/FormEquipment.jsx +++ b/client/src/components/admin/FormEquipment.jsx @@ -4,6 +4,8 @@ import { toast } from "react-toastify"; import { createEquipment, deleteEquipment } from "../../api/equipment"; import Uploadfile from "./Uploadfile"; import { Link } from "react-router-dom"; +import { Pencil } from "lucide-react"; +import { Trash2 } from "lucide-react"; const initialState = { title: "", @@ -20,11 +22,17 @@ const FormEquipment = () => { const getEquipment = useBorrowStock((state) => state.getEquipment); const equipments = useBorrowStock((state) => state.equipments); - const [form, setForm] = useState(initialState); + const [form, setForm] = useState({ + title: "", + description: "", + quantity: 0, + categoryId: "", + images: [], + }); useEffect(() => { - getCategory(token); - getEquipment(token, 100); + getCategory(); + getEquipment(100); }, []); const handleOnChange = (e) => { @@ -40,7 +48,7 @@ const FormEquipment = () => { const res = await createEquipment(token, form); console.log(res) setForm(initialState) - getEquipment(token) + getEquipment() toast.success(`เพิ่มข้อมูล ${res.data.title} สำเร็จ`); } catch (err) { console.log(err); @@ -54,7 +62,7 @@ const FormEquipment = () => { const res = await deleteEquipment(token, id) console.log(res) toast.success('Deleted Equipment already') - getEquipment(token) + getEquipment() }catch(err){ console.log(err) } @@ -125,9 +133,9 @@ const FormEquipment = () => { <br /> <div className="table-responsive"> - <table className="table"> + <table className="table w-full"> <thead> - <tr> + <tr className="table-secondary fw-bold"> <th scope="col">No.</th> <th scope="col">รูปภาพ</th> <th scope="col">ชื่ออุปกรณ์</th> @@ -165,19 +173,21 @@ const FormEquipment = () => { <td>{item.quantity}</td> <td>{item.borrowedCount}</td> <td>{item.updatedAt}</td> - <td className="flex gap-2"> - <Link - to={`/admin/equipment/${item.id}`} - className="btn btn-warning btn-sm" - > - แก้ไข - </Link> - <p - className="btn btn-danger btn-sm" - onClick={() => handleDelete(item.id)} - > - ลบ - </p> + <td> + <div className="d-flex gap-2"> + <Link + to={`/admin/equipment/${item.id}`} + className="btn btn-warning btn-sm px-3 edit-btn" + > + <Pencil /> + </Link> + <button + className="btn btn-danger btn-sm px-3 delete-btn" + onClick={() => handleDelete(item.id)} + > + <Trash2 /> + </button> + </div> </td> </tr> ))} diff --git a/client/src/components/admin/SidebarAdmin.jsx b/client/src/components/admin/SidebarAdmin.jsx index c5e2a315..811288b5 100644 --- a/client/src/components/admin/SidebarAdmin.jsx +++ b/client/src/components/admin/SidebarAdmin.jsx @@ -19,35 +19,22 @@ const SidebarAdmin = () => { {/* Navigation Links */} <nav className="nav flex-column my-3"> - <NavLink - to="/admin" - end - className="nav-link text-light d-flex align-items-center" - > + <NavLink to="/admin" end className="nav-link sidebar-link"> <LayoutDashboard className="me-2" /> Dashboard </NavLink> - <NavLink - to="manage" - className="nav-link text-light d-flex align-items-center" - > + <NavLink to="manage" className="nav-link sidebar-link"> <FolderKanban className="me-2" /> Manage </NavLink> - <NavLink - to="category" - className="nav-link text-light d-flex align-items-center" - > + <NavLink to="category" className="nav-link sidebar-link"> <ChartBarStacked className="me-2" /> Category </NavLink> - <NavLink - to="equipment" - className="nav-link text-light d-flex align-items-center" - > + <NavLink to="equipment" className="nav-link sidebar-link"> <BringToFront className="me-2" /> Equipment </NavLink> diff --git a/client/src/components/admin/Uploadfile.jsx b/client/src/components/admin/Uploadfile.jsx index 66dbd2db..4b00456a 100644 --- a/client/src/components/admin/Uploadfile.jsx +++ b/client/src/components/admin/Uploadfile.jsx @@ -3,12 +3,15 @@ import { toast } from "react-toastify"; import Resize from "react-image-file-resizer"; import { removeFiles, uploadFiles } from "../../api/equipment"; import useBorrowStock from "../../stock/borrow-stock"; +import { Loader } from "lucide-react"; const Uploadfile = ({ form, setForm }) => { const token = useBorrowStock((state) => state.token); const [isLoading, setIsLoading] = useState(false); const handleOnChange = (e) => { + setIsLoading(true) + const files = e.target.files; if (files) { setIsLoading(true); @@ -71,18 +74,22 @@ const Uploadfile = ({ form, setForm }) => { return ( <div className="my-4"> <div className="row"> + {isLoading && <Loader className="w-16 h-16 spinner-border" />} + {/* Images */} {form.images.map((item, index) => ( <div className="col-4 mb-3" key={index}> - <div className="position-relative"> + <div className="position-relative d-inline-block"> <img className="img-fluid rounded shadow" src={item.url} alt={`image-${index}`} + style={{ width: "150px", height: "150px", objectFit: "cover" }} /> <button onClick={() => handleDelete(item.public_id)} - className="position-absolute top-0 end-0 btn-close btn-danger" + className="position-absolute top-0 start-100 translate-middle btn-close btn-danger btn-sm p-1" + style={{ zIndex: 2 }} ></button> </div> </div> diff --git a/client/src/components/card/EquipmentCard.jsx b/client/src/components/card/EquipmentCard.jsx new file mode 100644 index 00000000..2e89c5c6 --- /dev/null +++ b/client/src/components/card/EquipmentCard.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import { SquarePlus } from "lucide-react"; + +const EquipmentCard = ({ item }) => { + return ( + <div + className="border rounded shadow p-1 col-12 col-sm-6 col-md-5 col-lg-4" + style={{ maxWidth: "400px" }} + > + {/* Image Placeholder */} + <div> + {item.images && item.images.length > 0 ? ( + <img + src={item.images[0].url} + alt={item.title} + className="img-fluid rounded mb-4" + style={{ + maxHeight: "200px", + objectFit: "cover", + width: "100%", + }} + /> + ) : ( + <div className="w-100 h-32 bg-secondary rounded text-center d-flex align-items-center justify-content-center shadow-sm mb-3"> + <span className="text-light">No Image</span> + </div> + )} + </div> + + {/* Title and Description */} + <div> + <p className="h5 font-weight-bold">{item.title}</p> + <p className="text-muted">{item.description}</p> + </div> + + {/* Action Button */} + <div className="d-flex justify-content-center align-items-center mt-3"> + <button className="btn btn-primary btn-sm"> + <SquarePlus className="me-2" /> + Add + </button> + </div> + </div> + ); +}; + +export default EquipmentCard; diff --git a/client/src/components/card/SearchCard.jsx b/client/src/components/card/SearchCard.jsx new file mode 100644 index 00000000..8f580182 --- /dev/null +++ b/client/src/components/card/SearchCard.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from "react"; +import useBorrowStock from "../../stock/borrow-stock"; +// import Slider from "rc-slider"; +// import "rc-slider/assets/index.css"; + + +const SearchCard = () => { + const getEquipment = useBorrowStock((state) => state.getEquipment); + const equipments = useBorrowStock((state) => state.equipments); + const actionSearchFilters = useBorrowStock((state) => state.actionSearchFilters); + const getCategory = useBorrowStock((state) => state.getCategory); + const categories = useBorrowStock((state) => state.categories); + + const [text, setText] = useState(""); + const [categorySelected, setCategorySelected] = useState([]) + + + // console.log(categories) + useEffect(()=>{ + getCategory() + },[]) + + +//search text + // console.log(text); + useEffect(()=>{ + const delay = setTimeout(()=>{ + if(text){ + actionSearchFilters({ query: text }); + } else{ + getEquipment(); + } + },300) + + return ()=> clearTimeout(delay) + },[text]) + +//search category +const handleCheck = (e)=>{ + // console.log(e.target.value) + const inCheck = e.target.value; //ค่าที่ติ้ก + const inState = [...categorySelected] //[] arr ว่าง + const findCheck = inState.indexOf(inCheck) //ถ้าไม่เจอจะ return -1 + + if(findCheck === -1){ + inState.push(inCheck) + } else{ + inState.splice(findCheck, 1) + } + + setCategorySelected(inState) + + if(inState.length > 0){ + actionSearchFilters({ category: inState }); + } else{ + getEquipment() + } +} +console.log(categorySelected) + + return ( + <div className="container mt-3"> + <h1 className="h4 fw-bold mb-3">ค้นหาอุปกรณ์</h1> + {/* search by text */} + <input + onChange={(e) => setText(e.target.value)} + type="text" + placeholder="ค้นหาอุปกรณ์..." + className="form-control mb-3" + /> + <hr /> + {/* search by category */} + <div> + <h4>หมวดหมู่อุปกรณ์</h4> + <div> + { + categories.map((item,index)=> + <div className="flex gap-2"> + <input + onChange={handleCheck} + value={item.id} + type="checkbox"/> + <label>{item.name}</label> + </div> + ) + } + </div> + </div> + + </div> + ); +}; + +export default SearchCard; diff --git a/client/src/index.css b/client/src/index.css index bd6213e1..e69de29b 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; \ No newline at end of file diff --git a/client/src/layouts/Layout.jsx b/client/src/layouts/Layout.jsx index 9f6ca43a..b6a4e159 100644 --- a/client/src/layouts/Layout.jsx +++ b/client/src/layouts/Layout.jsx @@ -9,7 +9,7 @@ const Layout = () => { return ( <div> <MainNav /> - <main> + <main className='h-full px-4 mt-2 mx-auto'> <Outlet /> </main> </div> diff --git a/client/src/pages/All.jsx b/client/src/pages/All.jsx index 73bd645d..7948f8e9 100644 --- a/client/src/pages/All.jsx +++ b/client/src/pages/All.jsx @@ -1,9 +1,46 @@ -import React from 'react' +import React, { useEffect } from "react"; +import EquipmentCard from "../components/card/EquipmentCard"; +import useBorrowStock from "../stock/borrow-stock"; +import SearchCard from "../components/card/SearchCard"; const All = () => { + const getEquipment = useBorrowStock((state) => state.getEquipment); + const equipments = useBorrowStock((state) => state.equipments); + + useEffect(() => { + getEquipment(); + }, []); + return ( - <div>All</div> - ) -} + <div className="container mt-4"> + <div className="row g-4"> + {/* SearchBar */} + <div className="col-12 col-md-3"> + <div className="bg-light p-3 rounded shadow-sm"> + <SearchCard /> + </div> + </div> + + {/* Equipment */} + <div className="col-12 col-md-6"> + <div className="bg-white p-3 rounded shadow-sm"> + <p>อุปกรณ์ทั้งหมด</p> + <div className="d-flex flex-wrap justify-content-between"> + {/* Equipment card */} + {equipments.map((item, index) => ( + <EquipmentCard key={index} item={item} /> + ))} + </div> + </div> + </div> + + {/* BorrowDetail */} + <div className="col-12 col-md-3"> + <div className="bg-light p-3 rounded shadow-sm">BorrowDetail</div> + </div> + </div> + </div> + ); +}; -export default All \ No newline at end of file +export default All; diff --git a/client/src/stock/borrow-stock.jsx b/client/src/stock/borrow-stock.jsx index bbcae45c..41613883 100644 --- a/client/src/stock/borrow-stock.jsx +++ b/client/src/stock/borrow-stock.jsx @@ -2,39 +2,47 @@ import axios from 'axios' import { create } from 'zustand' import { persist,createJSONStorage } from 'zustand/middleware' import { listCategory } from '../api/category' -import { listEquipment } from '../api/equipment' +import { listEquipment, searchFilters } from "../api/equipment"; -const borrowStock = (set)=> ({ - user: null, - token:null, - categories: [], - equipments:[], - actionLogin: async(form)=>{ - const res = await axios.post('http://localhost:3000/api/login', form) - console.log(res.data.token) - set({ - user: res.data.payload, - token: res.data.token - }) - return res - }, - getCategory : async(token,count)=>{ - try{ - const res = await listCategory(token,count) - set({categories: res.data}) - }catch(err){ - console.log(err) - } - }, - getEquipment : async(token)=>{ - try{ - const res = await listEquipment(token) - set({ equipments: res.data }); - }catch(err){ - console.log(err) - } - } -}) +const borrowStock = (set) => ({ + user: null, + token: null, + categories: [], + equipments: [], + actionLogin: async (form) => { + const res = await axios.post("http://localhost:3000/api/login", form); + console.log(res.data.token); + set({ + user: res.data.payload, + token: res.data.token, + }); + return res; + }, + getCategory: async () => { + try { + const res = await listCategory(); + set({ categories: res.data }); + } catch (err) { + console.log(err); + } + }, + getEquipment: async (count) => { + try { + const res = await listEquipment(count); + set({ equipments: res.data }); + } catch (err) { + console.log(err); + } + }, + actionSearchFilters: async (arg) => { + try { + const res = await searchFilters(arg); + set({ equipments: res.data }); + } catch (err) { + console.log(err); + } + }, +}); const usePersist = { name: 'borrow-stock', diff --git a/server/routes/category.js b/server/routes/category.js index 101a13b1..52301da9 100644 --- a/server/routes/category.js +++ b/server/routes/category.js @@ -5,7 +5,7 @@ const { authCheck, adminCheck } = require('../middlewares/authCheck') // @ENDPOINT http://localhost:3000/api/category router.post('/category', authCheck, adminCheck, create); -router.get('/category',authCheck, adminCheck, list); +router.get('/category', list); router.delete('/category/:id', authCheck, adminCheck, remove); module.exports = router; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 2f6cc72e..063828e2 100644 --- a/server/server.js +++ b/server/server.js @@ -6,6 +6,14 @@ const cors = require('cors'); // const authRouter = require('./routes/auth'); // const categoryRouter = require('./routes/category'); +const db = mysql.createConnection({ + host: "node77730-project-65160024.th2.melon.cloud", + port: "11818", + user: "root", + password: "YDGggh92238", + database: "borrow_project", +}); + //middleware app.use(morgan('dev')) app.use(express.json({ limit: "20mb" })); -- GitLab