diff --git a/README.md b/README.md new file mode 100644 index 0000000..16401ed --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +запуск: + +cd C:\Users\student\Desktop\ekz\back +node index.js + +C:\Users\student\Desktop\ekz\front +npm run dev \ No newline at end of file diff --git a/back/index.js b/back/index.js index 1c8ac76..af79ab1 100644 --- a/back/index.js +++ b/back/index.js @@ -1,5 +1,80 @@ -const app = require('express'); +const express = require('express'); + +const app = express(); const cors = require('cors'); const bcrypt = require('bcryptjs'); const mysql = require('mysql2'); - \ No newline at end of file + +const connection = mysql.createConnection({ + host: '192.168.201.207', + port: '3306', + user: 'pozorisp', + password: 'pozorisp', + database: 'pozorisp_Moskvin' +}); + +app.use(express.json()); +app.use(express.static('public')); +app.use(cors()); + +app.get('/material', (req, res) => { + connection.query( + "SELECT m.*, t.type as type, me.name as measure_name, me.code as measure_code FROM `material` as m LEFT JOIN `material_type` as t ON m.id_material_type = t.id LEFT JOIN `measure` as me ON m.id_measure = me.id", + (err, result) => { + if (err) { + return res.status(500).json({message: 'smth went wrong', data: null, error: err.message}); + } + + return res.status(200).json({message: 'ok', data: result}); + } + ); +}); + +app.get('/types', (req, res) => { + connection.query( + "SELECT id, type from `material_type`", + (err, result) => { + if (err) { + return res.status(500).json({message: 'smth went wrong', data: null, error: err.message}); + } + + return res.status(200).json({message: 'ok', data: result}); + } + ) +}); + +app.get('/measures', (req, res) => { + connection.query( + "SELECT id, name from `measure`", + (err, result) => { + if (err) { + return res.status(500).json({message: 'smth went wrong', data: null, error: err.message}); + } + + return res.status(200).json({message: 'ok', data: result}); + } + ) +}); + +app.post('/create', (req, res) => { + const {name, type, one_price, left, count_in_set, measure} = req.body; + + if (!name || !type || !one_price || !left || !count_in_set || !measure) { + return res.status(422).json({message: "Заполните все поля", data: null}); + } + + connection.query( + "INSERT INTO `material` (name, id_material_type, one_price, left, count_in_set, id_measure VALUES(?, ?, ?, ?)", + (err, result) => { + if (err) { + return res.status(500).json({message: 'smth went wrong', data: null, error: err.message}); + } + + return res.status(200).json({message: 'ok', data: result}); + } + ) +}); + +app.listen(3000, () => { + console.log('Running or port: ', 3000); +}); diff --git a/er.pdf b/er.pdf new file mode 100644 index 0000000..94b3516 Binary files /dev/null and b/er.pdf differ diff --git a/front/index.html b/front/index.html index b2f34e8..e47256a 100644 --- a/front/index.html +++ b/front/index.html @@ -2,7 +2,7 @@ - + front diff --git a/front/package-lock.json b/front/package-lock.json index 47c1a47..47dc115 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router": "^7.15.1" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -264,31 +265,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1056,6 +1032,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2110,6 +2099,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2117,6 +2107,28 @@ "react": "^19.2.6" } }, + "node_modules/react-router": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", @@ -2167,6 +2179,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/front/package.json b/front/package.json index 2991cb5..3c548a7 100644 --- a/front/package.json +++ b/front/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router": "^7.15.1" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/front/public/favicon.ico b/front/public/favicon.ico new file mode 100644 index 0000000..31513ba Binary files /dev/null and b/front/public/favicon.ico differ diff --git a/front/public/favicon.svg b/front/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/front/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/public/icons.svg b/front/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/front/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/front/src/App.css b/front/src/App.css index f90339d..5601b8f 100644 --- a/front/src/App.css +++ b/front/src/App.css @@ -1,184 +1,71 @@ -.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; - } +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: "Comic Sans MS"; } -.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); - } +body { + background-color: #FFFFFF; } -#center { +header { + background-color: #ABCFCE; +} + +.container { + padding: 20px; +} + +header .container { + display: flex; + align-items: center; + justify-content: space-between; +} + + +.materials-list { + margin-top: 30px; 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; - } + gap: 20px; } -#next-steps { +.material-card { + border: 1px solid #000; + padding: 5px 10px; 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; - } + align-items: center; + justify-content: space-between; } -#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; +.field { + margin-top: 15px; 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; - } - } + flex-direction: column; + width: fit-content; } -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } +.field input, +.field button, +.field a { + margin-top: 5px; + padding: 5px 10px; } -.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); - } +.field a { + color: #ffffff; + text-decoration: none; + background-color: #ABCFCE; + border-radius: 5px; } + +.field a:hover { + background: #83aaa9; +} + +.form { + display: flex; + flex-direction: column; + gap: 20px; +} \ No newline at end of file diff --git a/front/src/App.jsx b/front/src/App.jsx index 4f03aa1..3c029f8 100644 --- a/front/src/App.jsx +++ b/front/src/App.jsx @@ -1,122 +1,17 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import './App.css'; +import {BrowserRouter, Route, Routes} from 'react-router' +import { Home } from './pages/Home'; +import { CreateMaterial } from './pages/CreateMaterial'; function App() { - const [count, setCount] = useState(0) - - return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.jsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- - ) + return ( + <> + + } /> + } /> + + + ); } export default App diff --git a/front/src/assets/hero.png b/front/src/assets/hero.png deleted file mode 100644 index 02251f4..0000000 Binary files a/front/src/assets/hero.png and /dev/null differ diff --git a/front/src/assets/react.svg b/front/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/front/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/assets/vite.svg b/front/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/front/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/front/src/components/Header.tsx b/front/src/components/Header.tsx new file mode 100644 index 0000000..4fd04ce --- /dev/null +++ b/front/src/components/Header.tsx @@ -0,0 +1,13 @@ +import logo from '../images/logo.png'; + +export function Header() { + return ( +
+
+ + + Header +
+
+ ); +} \ No newline at end of file diff --git a/front/src/images/logo.png b/front/src/images/logo.png new file mode 100644 index 0000000..fdcf2fd Binary files /dev/null and b/front/src/images/logo.png differ diff --git a/front/src/index.css b/front/src/index.css index 2c84af0..e69de29 100644 --- a/front/src/index.css +++ b/front/src/index.css @@ -1,111 +0,0 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -body { - margin: 0; -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} diff --git a/front/src/main.jsx b/front/src/main.jsx index b9a1a6d..560babb 100644 --- a/front/src/main.jsx +++ b/front/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { BrowserRouter } from 'react-router' createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/front/src/pages/CreateMaterial.jsx b/front/src/pages/CreateMaterial.jsx new file mode 100644 index 0000000..354210e --- /dev/null +++ b/front/src/pages/CreateMaterial.jsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { Header } from "../components/Header"; +import { BASE_URL } from "./Home"; + +export function CreateMaterial() { + const [types, setTypes] = useState([]); + const [measures, setMeasures] = useState([]); + const [name, setName] = useState(); + const [type, setType] = useState(); + const [measure, setMeasure] = useState(); + const [price, setPrice] = useState(); + const [left, setLeft] = useState(); + const [inSet, setInSet] = useState(); + + const fetchTypes = async () => { + const res = await fetch(BASE_URL + 'types'); + const data = (await res.json()).data; + + setTypes(data); + } + + const fetchMeasures = async () => { + const res = await fetch(BASE_URL + 'measures'); + const data = (await res.json()).data; + + setMeasures(data); + } + + const onSubmit = () => { + fetch(BASE_URL + "create", { + method: "POST", + body: JSON.stringify({ + name, + one_price: price, + type, + measure, + count_in_set: inSet, + left + }) + }); + } + + useEffect(() => { + fetchTypes(); + fetchMeasures(); + }, []); + + return ( + <> +
+ +
+ + + + + + + + + + + + + +
+ + ) +} \ No newline at end of file diff --git a/front/src/pages/Home.jsx b/front/src/pages/Home.jsx new file mode 100644 index 0000000..b1901ef --- /dev/null +++ b/front/src/pages/Home.jsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from "react"; +import { Header } from "../components/Header"; +import { NavLink } from "react-router"; + +export const BASE_URL = "http://localhost:3000/"; + +export function Home() { + const [materials, setMaterials] = useState([]); + const [search, setSearch] = useState(""); + const [priceSort, setPriceSort] = useState(null); + + const fetchMaterials = async () => { + const res = await fetch(BASE_URL + 'material'); + let data = (await res.json()).data; + + if (search) { + data = data.filter(filterBySearch); + } + + if (priceSort) { + data = data.sort(sortByPrice); + } + + setMaterials(data); + } + + useEffect(() => { + fetchMaterials(); + }, [search, priceSort]); + + const filterBySearch = (material) => { + return material.name.toLowerCase().includes(search); + } + + const sortByPrice = (a, b) => { + if (!priceSort) return 0; + + const aPrice = calcPrice(a); + const bPrice = calcPrice(b); + + if (priceSort === 'desc') { + return Number.parseFloat(aPrice) - Number.parseFloat(bPrice); + } + + if (priceSort === 'asc') { + return Number.parseFloat(bPrice) - Number.parseFloat(aPrice); + } + } + + const calcPrice = (material) => { + const neededCount = material.minimal - material.left; + + if (neededCount <= 0) { + return 0; + } + + const needePrice = Math.ceil(neededCount / material.count_in_set) *material.count_in_set * material.one_price; + + return needePrice; + } + + const changeSort = () => { + if (!priceSort) { + setPriceSort('asc'); + }; + + if (priceSort === 'asc') { + setPriceSort('desc'); + } + + if (priceSort === 'desc') { + setPriceSort(null); + }; + } + + const currentSort = () => { + if (!priceSort) return 'Пусто'; + + if (priceSort === 'asc') return 'ASC'; + if (priceSort === 'desc') return 'DESC'; + } + + + + return ( + <> +
+ +
+

Список материалов

+ + + + + + + +
+ {materials.map((material) => ( +
+
+ {material.type} | {material.name}
+ Минимальное количество: {material.minimal} {material.measure_name}
+ Количество на складе: {material.left} {material.measure_name}
+ Цена: {material.one_price} р / Единица измерения: {material.measure_name}
+
+ + Стоимость партии: {new Intl.NumberFormat('ru-RU').format(calcPrice(material))} р +
+ ))} +
+
+ + ) +}