first commit
This commit is contained in:
6
web/.gitignore
vendored
Normal file
6
web/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/.parcel-cache
|
||||
/public/ais.js
|
||||
/build
|
||||
|
||||
6
web/Caddyfile
Normal file
6
web/Caddyfile
Normal file
@@ -0,0 +1,6 @@
|
||||
:8001 {
|
||||
route {
|
||||
reverse_proxy /api/* 127.0.0.1:8888
|
||||
reverse_proxy * 127.0.0.1:1234
|
||||
}
|
||||
}
|
||||
5979
web/package-lock.json
generated
Normal file
5979
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
web/package.json
Normal file
24
web/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.jsx --bundle --minify --outfile=public/ais.js --loader:.webp=dataurl --analyze",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/icons-material": "^5.10.16",
|
||||
"@mui/material": "^5.10.17",
|
||||
"esbuild": "^0.16.4",
|
||||
"parcel": "^2.8.1",
|
||||
"process": "^0.11.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.5"
|
||||
}
|
||||
}
|
||||
BIN
web/public/images/customer.webp
Normal file
BIN
web/public/images/customer.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
web/public/images/location.webp
Normal file
BIN
web/public/images/location.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/meeting.webp
Normal file
BIN
web/public/images/meeting.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
web/public/images/purchased.webp
Normal file
BIN
web/public/images/purchased.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
web/public/images/supplier.webp
Normal file
BIN
web/public/images/supplier.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
web/public/images/supplierGoods.webp
Normal file
BIN
web/public/images/supplierGoods.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
13
web/public/index.html
Normal file
13
web/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<h1 style="text-align: center;">Loading Sam's AIS...</h1>
|
||||
</div>
|
||||
<script type="module" src="/ais.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
web/src/common.js
Normal file
54
web/src/common.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const API_ENDPOINT = "/api";
|
||||
|
||||
const get = async (url) => {
|
||||
const resp = await fetch(`${API_ENDPOINT}${url}`);
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
const post = async (url, data) => {
|
||||
return await _post(url, data, "POST");
|
||||
};
|
||||
|
||||
const del = async (url, data) => {
|
||||
return await _post(url, data, "DELETE");
|
||||
};
|
||||
|
||||
const put = async (url, data) => {
|
||||
return await _post(url, data, "PUT");
|
||||
};
|
||||
|
||||
const _post = async (url, data, method) => {
|
||||
const resp = await fetch(`${API_ENDPOINT}${url}`, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.errors) {
|
||||
alert(json.errors);
|
||||
throw json.errors;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
const fixZero = (n) => {
|
||||
if (n < 10) return `0${n}`;
|
||||
return n;
|
||||
};
|
||||
|
||||
const time = (s) => {
|
||||
const t = new Date(s);
|
||||
const year = t.getFullYear();
|
||||
const month = fixZero(t.getMonth())
|
||||
const day = fixZero(t.getDay());
|
||||
const hours = fixZero(t.getHours());
|
||||
const minutes = fixZero(t.getMinutes());
|
||||
const second = fixZero(t.getSeconds());
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${second}`;
|
||||
};
|
||||
|
||||
export { get, post, del, put, time };
|
||||
38
web/src/components/AutoTable.jsx
Normal file
38
web/src/components/AutoTable.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
const AutoTable = ({ data }) => {
|
||||
const columns = Object.keys(data[0] || []);
|
||||
return (
|
||||
<TableContainer component={Paper} sx={{ m: 1 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col}>{col}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col}>{row[col]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoTable;
|
||||
22
web/src/components/ImageCard.jsx
Normal file
22
web/src/components/ImageCard.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
const ImageCard = ({ title, description, image, onClick }) => (
|
||||
<Card sx={{ m: 1 }}>
|
||||
<CardActionArea onClick={onClick}>
|
||||
<CardMedia component="img" height="239" image={image} alt="Card Image" />
|
||||
<CardContent>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<Typography>{description}</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default ImageCard;
|
||||
36
web/src/components/LoginButton.jsx
Normal file
36
web/src/components/LoginButton.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import userContext from "../context/userContext";
|
||||
import { get } from "../common";
|
||||
|
||||
const LoginButton = () => {
|
||||
const { user, setUser } = React.useContext(userContext);
|
||||
const navigator = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (user.role === 1) {
|
||||
navigator("/customer");
|
||||
} else if (user.role === 2) {
|
||||
navigator("/supplier");
|
||||
} else {
|
||||
navigator('/login');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
get("/login")
|
||||
.then((resp) => {
|
||||
setUser(resp);
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button variant="contained" color="secondary" onClick={handleClick}>
|
||||
{user.username || "Login"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginButton;
|
||||
31
web/src/components/LogoutCard.jsx
Normal file
31
web/src/components/LogoutCard.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { Card, CardActionArea, CardContent, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import { post } from "../common";
|
||||
import userContext from "../context/userContext";
|
||||
|
||||
const LogoutCard = () => {
|
||||
const nevigator = useNavigate();
|
||||
const { setUser } = React.useContext(userContext);
|
||||
|
||||
const handelLogout = async () => {
|
||||
await post("/logout", {});
|
||||
setUser({});
|
||||
nevigator("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{ m: 1 }}>
|
||||
<CardActionArea onClick={handelLogout}>
|
||||
<LogoutIcon />
|
||||
<CardContent>
|
||||
<Typography variant="h5">Logout</Typography>
|
||||
<Typography>Clear session</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutCard;
|
||||
23
web/src/components/SelectRole.jsx
Normal file
23
web/src/components/SelectRole.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { InputLabel, FormControl, Select, MenuItem } from "@mui/material";
|
||||
|
||||
const SelectRole = ({ inputRole, setInputRole }) => {
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-label">Role</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={inputRole}
|
||||
label="Role"
|
||||
onChange={(event) => setInputRole(event.target.value)}
|
||||
>
|
||||
<MenuItem value={1}>Customer</MenuItem>
|
||||
<MenuItem value={2}>Supplier</MenuItem>
|
||||
<MenuItem value={3}>Admin</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectRole;
|
||||
3
web/src/context/userContext.js
Normal file
3
web/src/context/userContext.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import {createContext} from 'react';
|
||||
const userContext = createContext({});
|
||||
export default userContext;
|
||||
12
web/src/index.html
Normal file
12
web/src/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
</div>
|
||||
<script type="module" src="./index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
44
web/src/index.jsx
Normal file
44
web/src/index.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createHashRouter as Router, RouterProvider } from "react-router-dom";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import RegisterPage from "./pages/RegisterPage";
|
||||
import CustomerPage from './pages/CustomerPage';
|
||||
import CustomerMarketPage from './pages/CustomerMarketPage';
|
||||
import userContext from "./context/userContext";
|
||||
import CustomerMarketDeatilPage from "./pages/CustomerMarketDetailPage";
|
||||
import CustomerPurchasedPage from "./pages/CustomerPurchasedPage";
|
||||
import SupplierPage from './pages/SupplierPage';
|
||||
import SupplierGoods from './pages/SupplierGoods';
|
||||
import SupplierAddGoods from './pages/SupplierAddGoods';
|
||||
|
||||
const router = Router([
|
||||
{ path: "/", element: <HomePage /> },
|
||||
{ path: "/ping", element: <div>pong</div> },
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/register", element: <RegisterPage /> },
|
||||
{ path: "/customer", element: <CustomerPage /> },
|
||||
{ path: "/customer/market", element: <CustomerMarketPage /> },
|
||||
{ path: "/customer/market/:marketid", element: <CustomerMarketDeatilPage /> },
|
||||
{ path: "/customer/purchased", element: <CustomerPurchasedPage /> },
|
||||
{ path: "/supplier", element: <SupplierPage /> },
|
||||
{ path: "/supplier/goods", element: <SupplierGoods /> },
|
||||
{ path: "/supplier/goods/new", element: <SupplierAddGoods /> },
|
||||
]);
|
||||
|
||||
const App = () => {
|
||||
const [user, setUser] = React.useState({});
|
||||
return (
|
||||
<userContext.Provider value={{ user, setUser }}>
|
||||
<RouterProvider router={router}></RouterProvider>
|
||||
</userContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
35
web/src/layouts/BasicLayout.jsx
Normal file
35
web/src/layouts/BasicLayout.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { AppBar, Box, CssBaseline, Toolbar, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import LoginButton from "../components/LoginButton";
|
||||
|
||||
const BasicLayout = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<header>
|
||||
<AppBar position="relative">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||
<Link style={{ textDecoration: "none", color: "unset" }} to="/">
|
||||
<img
|
||||
style={{
|
||||
height: "1.39rem",
|
||||
}}
|
||||
src="data:image/svg+xml;charset=utf-8,%3Csvg width='214' height='37' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M196.157.298l16.449 16.624c.75.758.75 1.987.001 2.745l-16.445 16.63-2.84-2.87a1.987 1.987 0 010-2.79l12.204-12.341-12.1-12.229a2.142 2.142 0 010-3.007l2.731-2.762zm-5.293 2.75l-2.719-2.75-16.362 16.546a2.075 2.075 0 000 2.911l16.367 16.542 2.716-2.747c.83-.84.83-2.2 0-3.038L178.781 18.3l12.084-12.22a2.161 2.161 0 00-.001-3.033zm-30.713 17.22c0 3.678-2.362 6.457-5.807 6.457-3.407 0-5.769-2.78-5.769-6.38 0-3.64 2.362-6.496 5.769-6.496 3.445 0 5.807 2.778 5.807 6.418zm-11.653 5.791c1.317 2.857 3.833 4.344 6.814 4.344 4.994 0 8.981-3.874 8.981-10.097 0-6.262-3.987-10.136-8.981-10.136-2.981 0-5.497 1.487-6.814 4.344V2.656l-3.871.783V30.09h3.871v-4.03zm-32.861-2.387c0 4.736 2.207 6.849 6.195 6.692.348 0 .735-.039 1.084-.117v-3.835h-1.007c-1.393 0-2.4-.861-2.4-2.74V2.656l-3.872.783v20.233zM69.951 4.847h4.18l-2.438 7.71H68.48l1.471-7.71zM51.521 30.05h3.87V19.523c0-3.209 1.975-5.713 4.801-5.713 2.594 0 3.872 1.878 3.872 4.343v11.898h3.87V18.193c0-4.97-2.98-8.062-7.045-8.062-2.594 0-4.762 1.135-6.388 3.718-1.161-2.349-3.29-3.718-5.884-3.718-2.478 0-4.607 1.37-5.768 3.835v-3.483h-3.872V30.05h3.872V19.523c0-3.209 1.974-5.713 4.8-5.713 2.594 0 3.871 1.878 3.871 4.343v11.898zM0 28.29l1.742-3.562c1.278.94 3.562 1.996 5.304 1.996 2.168 0 3.02-1.056 3.02-2.192.038-1.33-1.046-1.839-3.33-2.582-3.832-1.174-6-2.544-6-6.145 0-3.287 2.477-5.635 6.968-5.635 2.323 0 4.452.743 6.156 1.957l-1.743 3.248c-1.006-.626-2.71-1.566-4.607-1.566-1.664 0-2.67.666-2.67 1.957 0 1.057.735 1.761 3.174 2.505 4.142 1.252 6.194 2.818 6.194 6.105 0 3.718-2.981 5.988-7.124 5.988-2.826 0-5.535-.94-7.084-2.074zm25.628-14.44c-3.445 0-5.807 2.778-5.807 6.457 0 3.64 2.362 6.418 5.807 6.418 3.407 0 5.769-2.857 5.769-6.496 0-3.601-2.362-6.38-5.769-6.38zm5.844 12.21c-1.316 2.857-3.832 4.344-6.813 4.344-4.994 0-8.982-3.875-8.982-10.136 0-6.223 3.988-10.098 8.982-10.098 2.98 0 5.497 1.488 6.813 4.345v-4.031h3.872v19.568h-3.872V26.06zm43.01-1.332L72.74 28.29c1.549 1.135 4.259 2.074 7.085 2.074 4.142 0 7.123-2.27 7.123-5.988 0-3.287-2.052-4.853-6.194-6.105-2.439-.744-3.175-1.448-3.175-2.505 0-1.291 1.007-1.957 2.672-1.957 1.897 0 3.6.94 4.607 1.566l1.742-3.248a10.59 10.59 0 00-6.156-1.957c-4.49 0-6.968 2.348-6.968 5.635 0 3.6 2.168 4.97 6 6.145 2.284.743 3.368 1.252 3.33 2.582 0 1.136-.852 2.192-3.02 2.192-1.742 0-4.026-1.056-5.304-1.996zm21.991-4.46c0-6.223 4.259-10.098 10.182-10.098 2.787 0 5.265.9 7.162 2.818l-2.362 2.935c-1.471-1.487-3.213-2.074-4.762-2.074-3.561 0-6.078 2.152-6.078 6.38 0 4.226 2.517 6.456 6.078 6.456 1.549 0 3.291-.626 4.801-2.191l2.361 2.896c-1.897 1.917-4.336 2.974-7.2 2.974-5.885 0-10.182-3.874-10.182-10.097zm28.077 1.721V10.483h3.871v11.82c0 2.582 1.626 4.343 4.336 4.343 2.71 0 4.336-1.76 4.336-4.344V10.483h3.871V21.99c0 5.362-3.291 8.375-8.207 8.375-4.917 0-8.207-3.013-8.207-8.375z' fill='%23fff'/%3E%3C/svg%3E"
|
||||
/>
|
||||
</Link>
|
||||
</Typography>
|
||||
<LoginButton />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</header>
|
||||
<main>
|
||||
<Box sx={{ m: 3 }}>{children}</Box>
|
||||
</main>
|
||||
<footer></footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicLayout;
|
||||
13
web/src/layouts/SmallPaperLayout.jsx
Normal file
13
web/src/layouts/SmallPaperLayout.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import BasicLayout from "./BasicLayout";
|
||||
import { Paper, Container } from "@mui/material";
|
||||
|
||||
const SmallPaperLayout = ({ children }) => (
|
||||
<BasicLayout>
|
||||
<Container maxWidth="sm">
|
||||
<Paper sx={{ p: 3}}>{children}</Paper>
|
||||
</Container>
|
||||
</BasicLayout>
|
||||
);
|
||||
|
||||
export default SmallPaperLayout;
|
||||
56
web/src/pages/CustomerMarketDetailPage.jsx
Normal file
56
web/src/pages/CustomerMarketDetailPage.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import userContext from "../context/userContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { get, post } from "../common";
|
||||
|
||||
const CustomerMarketDeatilPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const [data, setData] = React.useState([]);
|
||||
const params = useParams();
|
||||
|
||||
const buyFunc = async (goodsid) => {
|
||||
await post(`/customer/market/${params.marketid}/${goodsid}`, {});
|
||||
refresh();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
get(`/customer/market/${params.marketid}`).then((resp) => {
|
||||
let goods = resp.goods.map((g) => {
|
||||
delete g.supplier_id;
|
||||
delete g.market_id;
|
||||
g.action = (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
buyFunc(g.id);
|
||||
}}
|
||||
>
|
||||
Buy!
|
||||
</Button>
|
||||
);
|
||||
return g;
|
||||
});
|
||||
setData(goods);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">
|
||||
Hi {user.username}, you are in market {params.marketid}
|
||||
</Typography>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerMarketDeatilPage;
|
||||
51
web/src/pages/CustomerMarketPage.jsx
Normal file
51
web/src/pages/CustomerMarketPage.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { TextField, Typography, Box, Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { Link } from "react-router-dom";
|
||||
import { get } from "../common";
|
||||
|
||||
const CustomerMarketPage = () => {
|
||||
const [inputPoint, setInputPoint] = React.useState("");
|
||||
const [data, setData] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
get("/customer/market").then((resp) => {
|
||||
const d = resp.markets.map((m) => {
|
||||
m.action = <Link to={`${m.id}`}>Enter</Link>;
|
||||
return m;
|
||||
});
|
||||
setData(d);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
get(`/customer/market/distance?point=${inputPoint}`).then((resp) => {
|
||||
const d = resp.markets.map((m) => {
|
||||
m.action = <Link to={`${m.id}`}>Enter</Link>;
|
||||
return m;
|
||||
});
|
||||
setData(d);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Find your neaset market</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex" }}>
|
||||
<TextField
|
||||
value={inputPoint}
|
||||
onChange={(event) => setInputPoint(event.target.value)}
|
||||
label="Location"
|
||||
/>
|
||||
<Button variant="contained" color="secondary" onClick={handleSearch}>
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerMarketPage;
|
||||
40
web/src/pages/CustomerPage.jsx
Normal file
40
web/src/pages/CustomerPage.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import userContext from "../context/userContext";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import LogoutCard from "../components/LogoutCard";
|
||||
|
||||
// images
|
||||
import locationImage from "../../public/images/location.webp";
|
||||
import purchasedImage from "../../public/images/purchased.webp";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
const CustomerPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Hello, customer {user.username}</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Link to="/customer/market">
|
||||
<ImageCard
|
||||
title="Nearby Market"
|
||||
description="Find your neaest market"
|
||||
image={locationImage}
|
||||
/>
|
||||
</Link>
|
||||
<Link to="/customer/purchased">
|
||||
<ImageCard
|
||||
title="Purchased"
|
||||
description="See what you have purchased"
|
||||
image={purchasedImage}
|
||||
/>
|
||||
</Link>
|
||||
<LogoutCard />
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerPage;
|
||||
75
web/src/pages/CustomerPurchasedPage.jsx
Normal file
75
web/src/pages/CustomerPurchasedPage.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import userContext from "../context/userContext";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { get, del, time } from "../common";
|
||||
|
||||
const CustomerPurchasedPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const [data, setData] = React.useState([]);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
del(`/customer/purchase/${id}`).then(() => {
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
get(`/customer/purchase`).then((resp) => {
|
||||
// flatten
|
||||
const result = resp.map((r) => {
|
||||
const { id, quantity, purchased_time } = r;
|
||||
const pt = time(purchased_time);
|
||||
const { name, price } = r.goods;
|
||||
const deleteButton = (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
handleDelete(id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
return {
|
||||
id,
|
||||
quantity,
|
||||
pt,
|
||||
name,
|
||||
price,
|
||||
action: deleteButton,
|
||||
};
|
||||
});
|
||||
setData(result);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">
|
||||
Hi {user.username}, here is what you have purchased
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
get(`/customer/purchase/report`).then((data) => {
|
||||
alert(`$The sum of all your perchase is ${data.sum}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Summary Report
|
||||
</Button>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerPurchasedPage;
|
||||
57
web/src/pages/HomePage.jsx
Normal file
57
web/src/pages/HomePage.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import userContext from "../context/userContext";
|
||||
|
||||
// images
|
||||
import CustomerImage from "../../public/images/customer.webp";
|
||||
import SupplierImage from "../../public/images/supplier.webp";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const HomePage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const navigator = useNavigate();
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Box sx={{ m: 3 }}>
|
||||
<Typography variant="h4">{"Welcome to sam's club"}</Typography>
|
||||
<Typography>Select a role to continue</Typography>
|
||||
<hr />
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<ImageCard
|
||||
title="Customer module"
|
||||
description="Find a neaest market and purchase goods"
|
||||
image={CustomerImage}
|
||||
onClick={() => {
|
||||
if (user.role !== 1) {
|
||||
alert("You are not allow to use this module");
|
||||
} else {
|
||||
navigator("/customer");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ImageCard
|
||||
title="Supplier"
|
||||
description="Manage the supply chain system and goods."
|
||||
image={SupplierImage}
|
||||
onClick={() => {
|
||||
if (user.role !== 2) {
|
||||
alert("You are not allow to use this module");
|
||||
} else {
|
||||
navigator("/supplier");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
80
web/src/pages/LoginPage.jsx
Normal file
80
web/src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import SelectRole from "../components/SelectRole";
|
||||
import userContext from "../context/userContext";
|
||||
import { post } from "../common";
|
||||
|
||||
const LoginPage = () => {
|
||||
const { setUser } = React.useContext(userContext);
|
||||
const [inputUsername, setInputUsername] = React.useState("");
|
||||
const [inputPassword, setInputPassword] = React.useState("");
|
||||
const [inputRole, setInputRole] = React.useState(0);
|
||||
|
||||
const validateInput = () => {
|
||||
if (!inputUsername) {
|
||||
alert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputUsername.length >= 30) {
|
||||
alert("Username too long");
|
||||
return;
|
||||
}
|
||||
if (!inputPassword) {
|
||||
alert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
if (!inputRole) {
|
||||
alert("Role can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// do login
|
||||
loginFunc();
|
||||
};
|
||||
|
||||
const navigator = useNavigate();
|
||||
|
||||
const loginFunc = async () => {
|
||||
const url = '/login';
|
||||
|
||||
const resp = await post(url, {
|
||||
username: inputUsername,
|
||||
password: inputPassword,
|
||||
});
|
||||
setUser(resp)
|
||||
|
||||
const nextURL = inputRole === 1 ? "/customer" : "/supplier";
|
||||
navigator(nextURL);
|
||||
};
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4" sx={{ pb: 2 }}>
|
||||
Login to Sam AIS
|
||||
</Typography>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="User name"
|
||||
value={inputUsername}
|
||||
onChange={(event) => setInputUsername(event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
value={inputPassword}
|
||||
onChange={(event) => setInputPassword(event.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
|
||||
<Button variant="contained" color="success" onClick={validateInput}>
|
||||
Login
|
||||
</Button>
|
||||
<Typography>
|
||||
Click <Link to="/register">here</Link> to register a new account
|
||||
</Typography>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
16
web/src/pages/ProfilePage.jsx
Normal file
16
web/src/pages/ProfilePage.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import SmallPaperLayout from '../layouts/SmallPaperLayout';
|
||||
import userContext from "../context/userContext";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
const ProfilePages = () => {
|
||||
const { user, setUser } = React.useContext(userContext);
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h3">Hello, {user.username}</Typography>
|
||||
<hr />
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePages;
|
||||
122
web/src/pages/RegisterPage.jsx
Normal file
122
web/src/pages/RegisterPage.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import SelectRole from "../components/SelectRole";
|
||||
import { post } from "../common";
|
||||
|
||||
const RegisterPage = () => {
|
||||
const [inputUsername, setInputUsername] = React.useState("");
|
||||
const [inputPassword, setInputPassword] = React.useState("");
|
||||
const [repeatInputPassword, setRepeatInputPassword] = React.useState("");
|
||||
const [inputRole, setInputRole] = React.useState(0);
|
||||
|
||||
const validateInput = () => {
|
||||
if (!inputUsername) {
|
||||
alert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputUsername.length >= 30) {
|
||||
alert("Username too long");
|
||||
return;
|
||||
}
|
||||
if (!inputPassword) {
|
||||
alert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputPassword !== repeatInputPassword) {
|
||||
alert("Password not matched!");
|
||||
return;
|
||||
}
|
||||
if (!inputRole) {
|
||||
alert("Role can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// do register
|
||||
registerFunc();
|
||||
};
|
||||
|
||||
const navigator = useNavigate();
|
||||
|
||||
const registerFunc = () => {
|
||||
const url = "/register";
|
||||
post(url, {
|
||||
username: inputUsername,
|
||||
password: inputPassword,
|
||||
role: inputRole,
|
||||
}).then(() => {
|
||||
alert("Successfully registered, please go to login page");
|
||||
navigator("/login");
|
||||
});
|
||||
};
|
||||
|
||||
const validatePassword = (p) => {
|
||||
if (p.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (p.length < 8) {
|
||||
return "password should be at least 8 characters";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4" sx={{ pb: 2 }}>
|
||||
Register
|
||||
</Typography>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="User name"
|
||||
value={inputUsername}
|
||||
onChange={(event) => setInputUsername(event.target.value)}
|
||||
error={inputUsername.length >= 30}
|
||||
helperText={inputUsername.length >= 30 && "User name too long !"}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
value={inputPassword}
|
||||
onChange={(event) => setInputPassword(event.target.value)}
|
||||
type="password"
|
||||
error={validatePassword(inputPassword)}
|
||||
helperText={validatePassword(inputPassword)}
|
||||
/>
|
||||
<TextField
|
||||
label="Repeat password"
|
||||
value={repeatInputPassword}
|
||||
onChange={(event) => setRepeatInputPassword(event.target.value)}
|
||||
type="password"
|
||||
error={inputPassword !== repeatInputPassword}
|
||||
helperText={
|
||||
inputPassword !== repeatInputPassword && "Password not matched"
|
||||
}
|
||||
/>
|
||||
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox />}
|
||||
label={
|
||||
<Typography>
|
||||
I agree the{" "}
|
||||
<Link to="/eula">end-user license agreement (EULA)</Link>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button variant="contained" color="warning" onClick={validateInput}>
|
||||
Register
|
||||
</Button>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterPage;
|
||||
67
web/src/pages/SupplierAddGoods.jsx
Normal file
67
web/src/pages/SupplierAddGoods.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import { post } from "../common";
|
||||
|
||||
const SupplierAddGoods = () => {
|
||||
const [inputName, setInputName] = React.useState("");
|
||||
const [inputDescription, setInputDescription] = React.useState("");
|
||||
const [inputMarketId, setInputMarketId] = React.useState(0);
|
||||
const [inputQuantity, setInputQuantity] = React.useState(0);
|
||||
const [inputPrice, setInputPrice] = React.useState(0);
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4">Add New Goods</Typography>
|
||||
<hr />
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
value={inputName}
|
||||
onChange={(event) => setInputName(event.target.value)}
|
||||
label="Product name"
|
||||
/>
|
||||
<TextField
|
||||
value={inputDescription}
|
||||
onChange={(event) => setInputDescription(event.target.value)}
|
||||
label="Product description"
|
||||
/>
|
||||
<TextField
|
||||
value={inputMarketId}
|
||||
onChange={(event) => setInputMarketId(event.target.value)}
|
||||
label="Market ID"
|
||||
type="number"
|
||||
/>
|
||||
<TextField
|
||||
value={inputQuantity}
|
||||
onChange={(event) => setInputQuantity(event.target.value)}
|
||||
label="Product quantity"
|
||||
type="number"
|
||||
/>
|
||||
<TextField
|
||||
value={inputPrice}
|
||||
onChange={(event) => setInputPrice(event.target.value)}
|
||||
label="Product price"
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
post(`/supplier/goods`, {
|
||||
name: inputName,
|
||||
description: inputDescription,
|
||||
market_id: parseInt(inputMarketId),
|
||||
quantity: parseInt(inputQuantity),
|
||||
price: inputPrice,
|
||||
}).then(() => {
|
||||
alert("Successfully add new goods");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierAddGoods;
|
||||
40
web/src/pages/SupplierGoods.jsx
Normal file
40
web/src/pages/SupplierGoods.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import { get } from "../common";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const SupplierGoods = () => {
|
||||
const [data, setData] = React.useState([]);
|
||||
const refresh = () => {
|
||||
get("/supplier/goods").then((resp) => {
|
||||
// delete unused data
|
||||
setData(
|
||||
resp.map((r) => {
|
||||
delete r.supplier_id;
|
||||
return r;
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Goods management</Typography>
|
||||
<Typography>The list of your goods</Typography>
|
||||
<Link to="/supplier/goods/new">
|
||||
<Button variant="contained" color="info">
|
||||
New Goods
|
||||
</Button>
|
||||
</Link>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierGoods;
|
||||
36
web/src/pages/SupplierPage.jsx
Normal file
36
web/src/pages/SupplierPage.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import supplierGoodsImage from "../../public/images/supplierGoods.webp";
|
||||
import meetingImage from "../../public/images/meeting.webp";
|
||||
import userContext from "../context/userContext";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const SupplierPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Welcome, supplier {user.username}</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Link to="/supplier/goods">
|
||||
<ImageCard
|
||||
title="Goods Management"
|
||||
description="Create, View, Update, and Delete your goods"
|
||||
image={supplierGoodsImage}
|
||||
/>
|
||||
</Link>
|
||||
<Link to="/supplier/report">
|
||||
<ImageCard
|
||||
title="Sales Report"
|
||||
description="Generate your fanaical report"
|
||||
image={meetingImage}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierPage;
|
||||
Reference in New Issue
Block a user