Karmashek commited on
Commit
6f4bcd4
·
verified ·
1 Parent(s): ca5ea58

Upload 21 files

Browse files
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ VITE_SUPABASE_URL=
2
+ VITE_SUPABASE_ANON_KEY=
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ node_modules
2
+ .env
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Используем официальный образ Node.js
2
+ FROM node:20-alpine
3
+
4
+ # Устанавливаем рабочую директорию
5
+ WORKDIR /usr/src/app
6
+
7
+ # Копируем package.json и устанавливаем зависимости
8
+ COPY package*.json ./
9
+ RUN npm install
10
+
11
+ # Копируем исходные файлы
12
+ COPY . .
13
+
14
+ # Экспортируем порт
15
+ EXPOSE 3000
16
+
17
+ # Запускаем приложение
18
+ CMD ["npm", "start"]
dist/assets/browser-BRznvupi.js ADDED
@@ -0,0 +1 @@
 
 
1
+ import{g as e}from"./index-B-0vcdVA.js";var o=function(){throw new Error("ws does not work in the browser. Browser clients must use the native WebSocket object")};const r=e(o),s=Object.freeze(Object.defineProperty({__proto__:null,default:r},Symbol.toStringTag,{value:"Module"}));export{s as b};
dist/assets/index-B-0vcdVA.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-B-2Hy4_l.css ADDED
@@ -0,0 +1 @@
 
 
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-16{margin-left:4rem}.ml-64{margin-left:16rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.flex{display:flex}.table{display:table}.grid{display:grid}.h-5{height:1.25rem}.h-64{height:16rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-16{width:4rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-6{gap:1.5rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-4{border-left-width:4px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-yellow-500{--tw-border-opacity: 1;border-color:rgb(234 179 8 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.bg-blue-800{--tw-bg-opacity: 1;background-color:rgb(30 64 175 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-opacity-70{--tw-bg-opacity: .7}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-2{padding-bottom:.5rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400}body{margin:0;min-width:320px;min-height:100vh}.notification-badge{position:absolute;top:0;right:0;font-size:.625rem;line-height:1.25rem;width:1.25rem;height:1.25rem;display:flex;align-items:center;justify-content:center;border-radius:9999px}.has-event:hover{background-color:#bfdbfe!important}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:text-blue-200:hover{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-red-800:hover{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
dist/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>MES System</title>
7
+ <script type="module" crossorigin src="/assets/index-B-0vcdVA.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-B-2Hy4_l.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
index.html CHANGED
@@ -1,29 +1,12 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
3
-
4
  <head>
5
- <meta charset="UTF-8" />
6
- <link rel="stylesheet" href="style.css" />
7
-
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- <title>Transformers.js - Object Detection</title>
10
  </head>
11
-
12
  <body>
13
- <h1>Object Detection w/ 🤗 Transformers.js</h1>
14
- <label id="container" for="upload">
15
- <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
16
- <path fill="#000"
17
- d="M3.5 24.3a3 3 0 0 1-1.9-.8c-.5-.5-.8-1.2-.8-1.9V2.9c0-.7.3-1.3.8-1.9.6-.5 1.2-.7 2-.7h18.6c.7 0 1.3.2 1.9.7.5.6.7 1.2.7 2v18.6c0 .7-.2 1.4-.7 1.9a3 3 0 0 1-2 .8H3.6Zm0-2.7h18.7V2.9H3.5v18.7Zm2.7-2.7h13.3c.3 0 .5 0 .6-.3v-.7l-3.7-5a.6.6 0 0 0-.6-.2c-.2 0-.4 0-.5.3l-3.5 4.6-2.4-3.3a.6.6 0 0 0-.6-.3c-.2 0-.4.1-.5.3l-2.7 3.6c-.1.2-.2.4 0 .7.1.2.3.3.6.3Z">
18
- </path>
19
- </svg>
20
- Click to upload image
21
- <label id="example">(or try example)</label>
22
- </label>
23
- <label id="status">Loading model...</label>
24
- <input id="upload" type="file" accept="image/*" />
25
-
26
- <script src="index.js" type="module"></script>
27
  </body>
28
-
29
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="ru">
 
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>MES System</title>
 
 
7
  </head>
 
8
  <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
 
 
 
 
 
 
 
 
 
 
 
 
11
  </body>
 
12
  </html>
index.js CHANGED
@@ -1,76 +1,3 @@
1
- import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.1';
2
 
3
- // Reference the elements that we will need
4
- const status = document.getElementById('status');
5
- const fileUpload = document.getElementById('upload');
6
- const imageContainer = document.getElementById('container');
7
- const example = document.getElementById('example');
8
-
9
- const EXAMPLE_URL = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/city-streets.jpg';
10
-
11
- // Create a new object detection pipeline
12
- status.textContent = 'Loading model...';
13
- const detector = await pipeline('object-detection', 'Xenova/detr-resnet-50');
14
- status.textContent = 'Ready';
15
-
16
- example.addEventListener('click', (e) => {
17
- e.preventDefault();
18
- detect(EXAMPLE_URL);
19
- });
20
-
21
- fileUpload.addEventListener('change', function (e) {
22
- const file = e.target.files[0];
23
- if (!file) {
24
- return;
25
- }
26
-
27
- const reader = new FileReader();
28
-
29
- // Set up a callback when the file is loaded
30
- reader.onload = e2 => detect(e2.target.result);
31
-
32
- reader.readAsDataURL(file);
33
- });
34
-
35
-
36
- // Detect objects in the image
37
- async function detect(img) {
38
- imageContainer.innerHTML = '';
39
- imageContainer.style.backgroundImage = `url(${img})`;
40
-
41
- status.textContent = 'Analysing...';
42
- const output = await detector(img, {
43
- threshold: 0.5,
44
- percentage: true,
45
- });
46
- status.textContent = '';
47
- output.forEach(renderBox);
48
- }
49
-
50
- // Render a bounding box and label on the image
51
- function renderBox({ box, label }) {
52
- const { xmax, xmin, ymax, ymin } = box;
53
-
54
- // Generate a random color for the box
55
- const color = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, 0);
56
-
57
- // Draw the box
58
- const boxElement = document.createElement('div');
59
- boxElement.className = 'bounding-box';
60
- Object.assign(boxElement.style, {
61
- borderColor: color,
62
- left: 100 * xmin + '%',
63
- top: 100 * ymin + '%',
64
- width: 100 * (xmax - xmin) + '%',
65
- height: 100 * (ymax - ymin) + '%',
66
- })
67
-
68
- // Draw label
69
- const labelElement = document.createElement('span');
70
- labelElement.textContent = label;
71
- labelElement.className = 'bounding-box-label';
72
- labelElement.style.backgroundColor = color;
73
-
74
- boxElement.appendChild(labelElement);
75
- imageContainer.appendChild(boxElement);
76
- }
 
1
+ // run `node index.js` in the terminal
2
 
3
+ console.log(`Hello Node.js v${process.versions.node}!`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mes-frontend",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "dev": "vite",
6
+ "build": "vite build",
7
+ "preview": "vite preview"
8
+ },
9
+ "dependencies": {
10
+ "react": "^18.2.0",
11
+ "react-dom": "^18.2.0",
12
+ "@supabase/supabase-js": "^2.39.3",
13
+ "chart.js": "^4.4.1",
14
+ "react-chartjs-2": "^5.2.0",
15
+ "sweetalert2": "^11.10.4",
16
+ "xlsx": "^0.18.5",
17
+ "@fortawesome/react-fontawesome": "^0.2.0",
18
+ "@fortawesome/free-solid-svg-icons": "^6.5.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.2.48",
22
+ "@types/react-dom": "^18.2.18",
23
+ "@vitejs/plugin-react": "^4.2.1",
24
+ "autoprefixer": "^10.4.17",
25
+ "postcss": "^8.4.33",
26
+ "tailwindcss": "^3.4.1",
27
+ "vite": "^5.0.12"
28
+ }
29
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/index.html ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>MES System</title>
7
+ <script src="https://cdn.tailwindcss.com "></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css "/>
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js "></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2 @11"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js "></script>
12
+ <style>
13
+ .notification-badge {
14
+ position: absolute;
15
+ top: 0; right: 0;
16
+ font-size: 0.625rem; line-height: 1.25rem;
17
+ width: 1.25rem; height: 1.25rem;
18
+ display: flex; align-items: center; justify-content: center;
19
+ border-radius: 9999px;
20
+ }
21
+ .has-event:hover { background-color: #BFDBFE !important; }
22
+ </style>
23
+ </head>
24
+ <body class="bg-gray-100 text-gray-900">
25
+
26
+ <!-- Login Modal -->
27
+ <div id="loginModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50">
28
+ <div class="bg-white rounded-lg shadow-lg w-full max-w-md p-6">
29
+ <h3 class="text-xl font-bold mb-4">Вход в систему</h3>
30
+ <form id="loginForm" class="space-y-4">
31
+ <input type="text" placeholder="Имя пользователя" required class="w-full px-4 py-2 border rounded-lg">
32
+ <input type="password" placeholder="Пароль" required class="w-full px-4 py-2 border rounded-lg">
33
+ <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg">Войти</button>
34
+ </form>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Sidebar -->
39
+ <div id="sidebar" class="fixed top-0 left-0 h-full bg-blue-800 text-white w-64 p-4 hidden">
40
+ <div class="flex items-center justify-between mb-6">
41
+ <h1 class="text-xl font-bold">MES System</h1>
42
+ <button onclick="toggleSidebar()" class="text-white hover:text-blue-200"><i class="fas fa-bars"></i></button>
43
+ </div>
44
+
45
+ <nav>
46
+ <ul class="space-y-2">
47
+ <li><button onclick="showTab('dashboard')" class="tab-btn active w-full text-left px-4 py-2 rounded-lg bg-blue-700"><i class="fas fa-tachometer-alt mr-2"></i> Главная</button></li>
48
+ <li><button onclick="showTab('schedule')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-calendar-alt mr-2"></i> График ТО</button></li>
49
+ <li><button onclick="showTab('equipment')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-tools mr-2"></i> Оборудование</button></li>
50
+ <li><button onclick="showTab('repairs')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-wrench mr-2"></i> Ремонт</button></li>
51
+ <li><button onclick="showTab('maintenance')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-clipboard-check mr-2"></i> Техобслуживание</button></li>
52
+ <li><button onclick="showTab('reports')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-chart-line mr-2"></i> Отчеты</button></li>
53
+ <li><button onclick="showTab('users')" class="tab-btn w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700"><i class="fas fa-users-cog mr-2"></i> Пользователи</button></li>
54
+ </ul>
55
+ </nav>
56
+ </div>
57
+
58
+ <!-- Main Content -->
59
+ <div id="mainContent" class="ml-64 p-6 min-h-screen hidden">
60
+ <!-- Header -->
61
+ <header class="bg-white shadow-sm p-4 rounded-lg mb-6 flex justify-between items-center">
62
+ <h1 id="page-title" class="text-2xl font-semibold">Главная панель</h1>
63
+ <div class="flex space-x-4">
64
+ <button class="p-2 rounded-full hover:bg-gray-100 relative">
65
+ <i class="fas fa-bell text-gray-600"></i>
66
+ <span class="notification-badge bg-red-500 text-white">3</span>
67
+ </button>
68
+ <div class="relative">
69
+ <input type="text" id="searchInput" oninput="filterEquipment()" placeholder="Поиск..." class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
70
+ <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
71
+ </div>
72
+ </div>
73
+ </header>
74
+
75
+ <!-- Tabs -->
76
+ <div id="content" class="bg-white rounded-lg shadow p-6">
77
+
78
+ <!-- Dashboard Tab -->
79
+ <div id="dashboard" class="tab-content active">
80
+ <h2 class="text-2xl font-bold mb-6">Главная панель</h2>
81
+
82
+ <!-- Stats Cards -->
83
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
84
+ <div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-500">
85
+ <h4 class="font-medium text-gray-500">Всего оборудования</h4>
86
+ <p id="total-equipment" class="text-2xl font-bold">0</p>
87
+ </div>
88
+ <div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-500">
89
+ <h4 class="font-medium text-gray-500">Выполнено ТО</h4>
90
+ <p id="completed-maintenance" class="text-2xl font-bold">0</p>
91
+ </div>
92
+ <div class="bg-white rounded-lg shadow p-6 border-l-4 border-yellow-500">
93
+ <h4 class="font-medium text-gray-500">Просрочено ТО</h4>
94
+ <p id="overdue-maintenance" class="text-2xl font-bold">0</p>
95
+ </div>
96
+ <div class="bg-white rounded-lg shadow p-6 border-l-4 border-red-500">
97
+ <h4 class="font-medium text-gray-500">Текущие ремонты</h4>
98
+ <p id="current-repairs" class="text-2xl font-bold">0</p>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Equipment Table -->
103
+ <div class="bg-white rounded-lg shadow overflow-hidden">
104
+ <div class="p-4 border-b flex justify-between items-center">
105
+ <h2 class="text-lg font-semibold">Оборудование</h2>
106
+ <a href="#equipment" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">Перейти к оборудованию</a>
107
+ </div>
108
+ <div class="overflow-x-auto">
109
+ <table id="dashboard-table" class="min-w-full divide-y divide-gray-200">
110
+ <thead class="bg-gray-50">
111
+ <tr>
112
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">ID</th>
113
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Название</th>
114
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Статус</th>
115
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Последнее ТО</th>
116
+ </tr>
117
+ </thead>
118
+ <tbody id="dashboard-body" class="bg-white divide-y divide-gray-200"></tbody>
119
+ </table>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Equipment Tab -->
125
+ <div id="equipment" class="tab-content hidden">
126
+ <div class="mb-4 flex justify-between items-center">
127
+ <h2 class="text-2xl font-bold">Оборудование</h2>
128
+ <button onclick="openAddEquipmentModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center">
129
+ <i class="fas fa-plus mr-2"></i> Добавить оборудование
130
+ </button>
131
+ </div>
132
+
133
+ <div class="mb-4 flex space-x-4">
134
+ <select id="statusFilter" onchange="filterEquipment()" class="border rounded-lg px-4 py-2">
135
+ <option value="">Все статусы</option>
136
+ <option value="active">Активно</option>
137
+ <option value="maintenance">ТО</option>
138
+ <option value="repair">Ремонт</option>
139
+ </select>
140
+ <select id="typeFilter" onchange="filterEquipment()" class="border rounded-lg px-4 py-2">
141
+ <option value="">Все типы</option>
142
+ <option>Станок</option>
143
+ <option>Конвейер</option>
144
+ <option>Компрессор</option>
145
+ <option>Генератор</option>
146
+ <option>Насос</option>
147
+ </select>
148
+ <input type="text" id="inventoryFilter" oninput="filterEquipment()" placeholder="Инвентарный номер" class="border rounded-lg px-4 py-2 w-1/4">
149
+ <div class="flex items-center space-x-2">
150
+ <input type="date" id="dateFrom" oninput="filterEquipment()" class="border rounded-lg px-4 py-2">
151
+ <span>-</span>
152
+ <input type="date" id="dateTo" oninput="filterEquipment()" class="border rounded-lg px-4 py-2">
153
+ </div>
154
+ </div>
155
+
156
+ <div class="bg-white rounded-lg shadow overflow-hidden">
157
+ <div class="overflow-x-auto">
158
+ <table class="min-w-full divide-y divide-gray-200">
159
+ <thead class="bg-gray-50">
160
+ <tr>
161
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">ID</th>
162
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Название</th>
163
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Тип</th>
164
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Статус</th>
165
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Ответственный</th>
166
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Действия</th>
167
+ </tr>
168
+ </thead>
169
+ <tbody id="equipment-list" class="bg-white divide-y divide-gray-200"></tbody>
170
+ </table>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <!-- Schedule Tab -->
176
+ <div id="schedule" class="tab-content hidden">
177
+ <h2 class="text-2xl font-bold mb-6">График ТО</h2>
178
+ <div class="bg-white rounded-lg shadow p-6">
179
+ <h3 class="text-lg font-semibold mb-4">Календарь ТО</h3>
180
+ <div class="grid grid-cols-7 gap-1 mb-2">
181
+ <div class="text-center text-sm font-medium text-gray-500">Пн</div>
182
+ <div class="text-center text-sm font-medium text-gray-500">Вт</div>
183
+ <div class="text-center text-sm font-medium text-gray-500">Ср</div>
184
+ <div class="text-center text-sm font-medium text-gray-500">Чт</div>
185
+ <div class="text-center text-sm font-medium text-gray-500">Пт</div>
186
+ <div class="text-center text-sm font-medium text-gray-500">Сб</div>
187
+ <div class="text-center text-sm font-medium text-gray-500">Вс</div>
188
+ </div>
189
+ <div id="calendar" class="grid grid-cols-7 gap-1 h-64 overflow-y-auto"></div>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Maintenance Tab -->
194
+ <div id="maintenance" class="tab-content hidden">
195
+ <h2 class="text-2xl font-bold mb-6">Техническое обслуживание</h2>
196
+ <div class="bg-white rounded-lg shadow p-6">
197
+ <h3 class="text-lg font-semibold mb-4">Задачи технического обслуживания</h3>
198
+ <div class="space-y-4" id="maintenanceTasks"></div>
199
+ </div>
200
+ </div>
201
+
202
+ <!-- Reports Tab -->
203
+ <div id="reports" class="tab-content hidden">
204
+ <h2 class="text-2xl font-bold mb-6">Отчеты</h2>
205
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
206
+ <div class="bg-white rounded-lg shadow p-6">
207
+ <h3 class="text-lg font-semibold mb-4">Статистика ТО по месяцам</h3>
208
+ <canvas id="maintenanceChart" width="400" height="250"></canvas>
209
+ </div>
210
+ <div class="bg-white rounded-lg shadow p-6">
211
+ <h3 class="text-lg font-semibold mb-4">Статус оборудования</h3>
212
+ <canvas id="equipmentStatusChart" width="400" height="250"></canvas>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Users Tab -->
218
+ <div id="users" class="tab-content hidden">
219
+ <h2 class="text-2xl font-bold mb-6">Пользователи</h2>
220
+ <div class="bg-white rounded-lg shadow p-6">
221
+ <table class="min-w-full divide-y divide-gray-200">
222
+ <thead class="bg-gray-50">
223
+ <tr>
224
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Имя</th>
225
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Email</th>
226
+ <th class="px-6 py-3 text-left text-xs uppercase tracking-wider">Роль</th>
227
+ </tr>
228
+ </thead>
229
+ <tbody id="user-list" class="bg-white divide-y divide-gray-200"></tbody>
230
+ </table>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ <!-- Add Equipment Modal -->
237
+ <div id="addEquipmentModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
238
+ <div class="bg-white rounded-lg shadow-lg w-full max-w-2xl">
239
+ <div class="p-4 border-b flex justify-between items-center">
240
+ <h3 class="text-lg font-semibold">Добавить новое оборудование</h3>
241
+ <button onclick="closeAddEquipmentModal()" class="text-gray-500 hover:text-gray-700">&times;</button>
242
+ </div>
243
+ <form id="equipmentForm" class="p-6 space-y-4">
244
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
245
+ <div>
246
+ <label class="block text-sm font-medium text-gray-700 mb-1">Название оборудования</label>
247
+ <input type="text" name="name" required class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
248
+ </div>
249
+ <div>
250
+ <label class="block text-sm font-medium text-gray-700 mb-1">Тип оборудования</label>
251
+ <select name="type" required class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
252
+ <option value="">Выберите тип</option>
253
+ <option>Станок</option>
254
+ <option>Конвейер</option>
255
+ <option>Компрессор</option>
256
+ <option>Генератор</option>
257
+ <option>Насос</option>
258
+ </select>
259
+ </div>
260
+ <div>
261
+ <label class="block text-sm font-medium text-gray-700 mb-1">Серийный номер</label>
262
+ <input type="text" name="serial" required class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
263
+ </div>
264
+ <div>
265
+ <label class="block text-sm font-medium text-gray-700 mb-1">Инвентарный номер</label>
266
+ <input type="text" name="inventory" required class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
267
+ </div>
268
+ <div>
269
+ <label class="block text-sm font-medium text-gray-700 mb-1">Дата ввода в эксплуатацию</label>
270
+ <input type="date" name="commissionDate" required class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
271
+ </div>
272
+ <div>
273
+ <label class="block text-sm font-medium text-gray-700 mb-1">Периодичность ТО (дни)</label>
274
+ <input type="number" name="interval" required min="1" class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500">
275
+ </div>
276
+ <div class="md:col-span-2">
277
+ <label class="block text-sm font-medium text-gray-700 mb-1">О��исание</label>
278
+ <textarea name="description" rows="3" class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500"></textarea>
279
+ </div>
280
+ </div>
281
+ <div class="mt-4 flex justify-end space-x-2">
282
+ <button type="button" onclick="closeAddEquipmentModal()" class="border px-4 py-2 rounded hover:bg-gray-100">Отмена</button>
283
+ <button type="submit" onclick="saveEquipment(event)" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Сохранить</button>
284
+ </div>
285
+ </form>
286
+ </div>
287
+ </div>
288
+
289
+ <script>
290
+ // Load data from localStorage or use defaultconst equipmentData = JSON.parse(localStorage.getItem("equipment")) || [{id: 1,name: "5-ти осевой фрезерный центр Nmill 1400",type: "Станок",serial: "CNC2000-001",inventory: "INV-1001",status: "active",lastMaintenance: "",nextMaintenance: "2023-06-15",maintenanceInterval: 30,description: "5-ти осевой фрезерный центр"},{id: 2,name: "3-х осевой фрезерный центр Mikron HPM 600HD",type: "Станок",serial: "CONV-A12-045",inventory: "INV-1042",status: "active",lastMaintenance: "",nextMaintenance: "2023-06-20",maintenanceInterval: 30,description: "3-х осевой фрезерный центр"}];let maintenanceEvents = JSON.parse(localStorage.getItem("maintenanceEvents")) || [{equipmentId: 1,date: "2023-06-15",status: "planned",responsible: "Иванов А.П."},{equipmentId: 2,date: "2023-06-20",status: "planned",responsible: "Петров И.С."}];let repairEvents = JSON.parse(localStorage.getItem("repairEvents")) || [];let currentEditingEquipmentId = null;let currentEditingRepairId = null;function saveToLocalStorage() {localStorage.setItem("equipment", JSON.stringify(equipmentData));localStorage.setItem("maintenanceEvents", JSON.stringify(maintenanceEvents));localStorage.setItem("repairEvents", JSON.stringify(repairEvents));}
291
+
292
+ function formatDate(dateStr) {
293
+ if (!dateStr) return "";
294
+ const d = new Date(dateStr);
295
+ return `${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`;
296
+ }
297
+
298
+ function calculateNextMaintenanceDate(commissionDate, intervalDays) {
299
+ if (!commissionDate) return '';
300
+ const nextDate = new Date(new Date(commissionDate).getTime() + parseInt(intervalDays)*24*60*60*1000);
301
+ return nextDate.toISOString().split('T')[0];
302
+ }
303
+
304
+ function toggleSidebar() {
305
+ const sidebar = document.getElementById("sidebar");
306
+ const isCollapsed = sidebar.classList.contains("collapsed");
307
+ sidebar.classList.toggle("collapsed", !isCollapsed);
308
+ document.querySelector(".ml-64").classList.toggle("ml-16", !isCollapsed);
309
+ }
310
+
311
+ function showTab(tabId) {
312
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
313
+ document.getElementById(tabId).classList.remove('hidden');
314
+ document.getElementById('page-title').innerText = tabId === 'dashboard' ? 'Главная панель' : tabId === 'schedule' ? 'График ТО' : tabId.charAt(0).toUpperCase() + tabId.slice(1);
315
+
316
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('bg-blue-700'));
317
+ event.target.classList.add('bg-blue-700');
318
+ }
319
+
320
+ function openAddEquipmentModal() {
321
+ document.getElementById("addEquipmentModal").classList.remove("hidden");
322
+ }
323
+
324
+ function closeAddEquipmentModal() {
325
+ document.getElementById("addEquipmentModal").classList.add("hidden");
326
+ document.getElementById("equipmentForm").reset();
327
+ }
328
+
329
+ function saveEquipment(event) {
330
+ event.preventDefault();
331
+ const form = document.getElementById("equipmentForm");
332
+ const formData = new FormData(form);
333
+ const name = formData.get("name");
334
+ const type = formData.get("type");
335
+ const serial = formData.get("serial");
336
+ const inventory = formData.get("inventory");
337
+ const commissionDate = formData.get("commissionDate");
338
+ const interval = formData.get("interval");
339
+ const description = formData.get("description");
340
+
341
+ if (!name || !type || !serial || !inventory || !commissionDate || !interval) {
342
+ Swal.fire({ title: 'Ошибка', text: 'Заполните все обязательные поля', icon: 'error' });
343
+ return;
344
+ }
345
+
346
+ const newEq = {
347
+ id: Math.max(...equipmentData.map(e => e.id)) + 1,
348
+ name, type, serial, inventory, status: "active",
349
+ lastMaintenance: "",
350
+ nextMaintenance: calculateNextMaintenanceDate(commissionDate, interval),
351
+ maintenanceInterval: interval,
352
+ description
353
+ };
354
+
355
+ equipmentData.push(newEq);
356
+ closeAddEquipmentModal();
357
+ renderAll();
358
+ Swal.fire({ title: 'Успешно', text: 'Оборудование добавлено', icon: 'success' });
359
+ }
360
+
361
+ function editEquipment(id) {
362
+ const equipment = equipmentData.find(eq => eq.id === id);
363
+ currentEditingEquipmentId = id;
364
+ document.getElementById("editName").value = equipment.name;
365
+ document.getElementById("editType").value = equipment.type;
366
+ document.getElementById("editSerial").value = equipment.serial;
367
+ document.getElementById("editInventory").value = equipment.inventory;
368
+ document.getElementById("editCommissionDate").value = equipment.commissionDate;
369
+ document.getElementById("editMaintenanceInterval").value = equipment.maintenanceInterval;
370
+ document.getElementById("editDescription").value = equipment.description;
371
+ document.getElementById("editEquipmentModal").classList.remove("hidden");
372
+ }
373
+
374
+ function deleteEquipment(id) {
375
+ Swal.fire({
376
+ title: 'Подтвердите удаление',
377
+ text: 'Вы уверены, что хотите удалить это оборудование?',
378
+ icon: 'warning',
379
+ showCancelButton: true,
380
+ confirmButtonText: 'Да, удалить',
381
+ cancelButtonText: 'Отмена'
382
+ }).then(result => {
383
+ if (result.isConfirmed) {
384
+ equipmentData = equipmentData.filter(eq => eq.id !== id);
385
+ renderAll();
386
+ Swal.fire('Удалено!', 'Оборудование успешно удалено.', 'success');
387
+ }
388
+ });
389
+ }
390
+
391
+ function generateCalendar() {
392
+ const calendar = document.getElementById("calendar");
393
+ const today = new Date();
394
+ const year = today.getFullYear();
395
+ const month = today.getMonth();
396
+ const firstDay = new Date(year, month, 1).getDay();
397
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
398
+ const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1;
399
+ calendar.innerHTML = '';
400
+
401
+ for (let i = 0; i < 42; i++) {
402
+ const day = i - adjustedFirstDay + 1;
403
+ const cell = document.createElement("div");
404
+ if (day > 0 && day <= daysInMonth) {
405
+ const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
406
+ cell.textContent = day;
407
+ cell.className = "h-10 flex items-center justify-center border rounded cursor-pointer";
408
+ if (maintenanceEvents.some(ev => ev.date === dateStr)) {
409
+ cell.style.backgroundColor = "#BFDBFE";
410
+ }
411
+ cell.onclick = () => showDayEvents(day);
412
+ } else {
413
+ cell.textContent = "";
414
+ }
415
+ calendar.appendChild(cell);
416
+ }
417
+ }
418
+
419
+ function showDayEvents(day) {
420
+ const today = new Date();
421
+ const year = today.getFullYear();
422
+ const month = today.getMonth();
423
+ const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
424
+ const events = maintenanceEvents.filter(ev => ev.date === dateStr);
425
+ let html = `<h3 class="font-bold mb-2">${day} ${getMonthName(month)} ${year}</h3>`;
426
+ html += '<div class="space-y-2">';
427
+ events.forEach(event => {
428
+ const equipment = equipmentData.find(e => e.id === event.equipmentId);
429
+ html += `<div class="p-2 border rounded">${equipment?.name || 'Неизвестное оборудование'}<br><small>${event.responsible}</small></div>`;
430
+ });
431
+ html += '</div>';
432
+ if (events.length === 0) {
433
+ html = `<h3 class="font-bold mb-2">${day} ${getMonthName(month)} ${year}</h3><p>Нет запланированных работы</p>`;
434
+ }
435
+ Swal.fire({ html, icon: 'info', showConfirmButton: false, width: '500px' });
436
+ }
437
+
438
+ function getMonthName(monthIndex) {
439
+ const months = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июл','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
440
+ return months[monthIndex];
441
+ }
442
+
443
+ function updateDashboardStats() {
444
+ document.getElementById("total-equipment").textContent = equipmentData.length;
445
+ document.getElementById("completed-maintenance").textContent = equipmentData.filter(e => e.lastMaintenance).length;
446
+ document.getElementById("overdue-maintenance").textContent = equipmentData.filter(e => new Date(e.nextMaintenance) < new Date()).length;
447
+ document.getElementById("current-repairs").textContent = equipmentData.filter(e => e.status === "repair").length;
448
+ }
449
+
450
+ function renderEquipmentTable() {
451
+ const tbody = document.getElementById("equipment-list");
452
+ const dashboardTbody = document.getElementById("dashboard-body");
453
+ tbody.innerHTML = '';
454
+ dashboardTbody.innerHTML = '';
455
+
456
+ const filterStatus = document.getElementById("statusFilter")?.value || '';
457
+ const filterType = document.getElementById("typeFilter")?.value || '';
458
+ const filterInventory = document.getElementById("inventoryFilter")?.value.toLowerCase() || '';
459
+ const filterId = document.getElementById("idFilter")?.value || '';
460
+ const filterDateFrom = document.getElementById("dateFrom")?.value;
461
+ const filterDateTo = document.getElementById("dateTo")?.value;
462
+
463
+ equipmentData.filter(eq => {
464
+ return (
465
+ (!filterStatus || eq.status === filterStatus) &&
466
+ (!filterType || eq.type === filterType) &&
467
+ (!filterInventory || eq.inventory.toLowerCase().includes(filterInventory)) &&
468
+ (!filterId || eq.id.toString().includes(filterId)) &&
469
+ (!filterDateFrom || new Date(eq.nextMaintenance) >= new Date(filterDateFrom)) &&
470
+ (!filterDateTo || new Date(eq.nextMaintenance) <= new Date(filterDateTo))
471
+ );
472
+ }).forEach(eq => {
473
+ const row = document.createElement("tr");
474
+ row.innerHTML = `
475
+ <td class="px-6 py-4">${eq.id}</td>
476
+ <td class="px-6 py-4">${eq.name}</td>
477
+ <td class="px-6 py-4">${eq.type}</td>
478
+ <td class="px-6 py-4">
479
+ <span class="px-2 py-1 rounded-full text-xs font-semibold ${eq.status === 'active' ? 'bg-green-100 text-green-800' : eq.status === 'maintenance' ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'}">
480
+ ${eq.status === 'active' ? 'Активно' : eq.status === 'maintenance' ? 'ТО' : 'Ремонт'}
481
+ </span>
482
+ </td>
483
+ <td class="px-6 py-4">
484
+ <button onclick="editEquipment(${eq.id})" class="text-blue-600 hover:text-blue-800 mr-3"><i class="fas fa-edit"></i></button>
485
+ <button onclick="deleteEquipment(${eq.id})" class="text-red-600 hover:text-red-800"><i class="fas fa-trash"></i></button>
486
+ </td>
487
+ `;
488
+ tbody.appendChild(row);
489
+
490
+ const dashRow = document.createElement("tr");
491
+ dashRow.innerHTML = `
492
+ <td class="px-6 py-4">${eq.id}</td>
493
+ <td class="px-6 py-4">${eq.name}</td>
494
+ <td class="px-6 py-4">
495
+ <span class="px-2 py-1 rounded-full text-xs font-semibold ${eq.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
496
+ ${eq.status === 'active' ? 'Активно' : 'ТО'}
497
+ </span>
498
+ </td>
499
+ <td class="px-6 py-4">${formatDate(eq.lastMaintenance)}</td>
500
+ `;
501
+ dashboardTbody.appendChild(dashRow);
502
+ });
503
+ }
504
+
505
+ function exportFilteredToExcel() {
506
+ const wb = XLSX.utils.table_to_book(document.getElementById("equipment-table"));
507
+ XLSX.writeFile(wb, "filtered-equipment.xlsx");
508
+ }
509
+
510
+ function logout() {
511
+ localStorage.removeItem("loggedInUser");
512
+ location.reload();
513
+ }
514
+
515
+ function renderAll() {
516
+ renderEquipmentTable();
517
+ updateDashboardStats();
518
+ generateCalendar();
519
+ }
520
+
521
+ function initApp() {
522
+ const loggedInUser = localStorage.getItem("loggedInUser");
523
+ if (!loggedInUser) {
524
+ document.getElementById("loginModal").classList.remove("hidden");
525
+ } else {
526
+ document.getElementById("loginModal").remove();
527
+ document.getElementById("mainContent").classList.remove("hidden");
528
+ document.getElementById("sidebar").classList.remove("hidden");
529
+ renderAll();
530
+ }
531
+ }
532
+
533
+ document.getElementById("loginForm")?.addEventListener("submit", function(e) {
534
+ e.preventDefault();
535
+ const username = this[0].value;
536
+ const password = this[1].value;
537
+ if (username && password) {
538
+ localStorage.setItem("loggedInUser", "admin");
539
+ document.getElementById("loginModal").classList.add("hidden");
540
+ renderAll();
541
+ }
542
+ });
543
+
544
+ window.onload = initApp;
545
+ </script>
src/App.jsx ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
+ import { faTachometerAlt, faCalendarAlt, faTools, faWrench, faClipboardCheck, faChartLine, faUsersCog, faBell, faBars } from '@fortawesome/free-solid-svg-icons';
5
+ import Swal from 'sweetalert2';
6
+ import Dashboard from './components/Dashboard';
7
+ import Equipment from './components/Equipment';
8
+
9
+ // Initialize Supabase client
10
+ const supabase = createClient(
11
+ import.meta.env.VITE_SUPABASE_URL,
12
+ import.meta.env.VITE_SUPABASE_ANON_KEY
13
+ );
14
+
15
+ function App() {
16
+ const [currentTab, setCurrentTab] = useState('dashboard');
17
+ const [equipment, setEquipment] = useState([]);
18
+ const [maintenanceEvents, setMaintenanceEvents] = useState([]);
19
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
20
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
21
+
22
+ useEffect(() => {
23
+ const checkUser = async () => {
24
+ const { data: { user } } = await supabase.auth.getUser();
25
+ if (user) {
26
+ setIsLoggedIn(true);
27
+ loadData();
28
+ }
29
+ };
30
+ checkUser();
31
+ }, []);
32
+
33
+ const loadData = async () => {
34
+ try {
35
+ const [equipmentResult, eventsResult] = await Promise.all([
36
+ supabase.from('equipment').select('*').order('created_at', { ascending: false }),
37
+ supabase.from('maintenance_events').select('*').order('date', { ascending: false })
38
+ ]);
39
+
40
+ if (equipmentResult.error) throw equipmentResult.error;
41
+ if (eventsResult.error) throw eventsResult.error;
42
+
43
+ setEquipment(equipmentResult.data || []);
44
+ setMaintenanceEvents(eventsResult.data || []);
45
+ } catch (error) {
46
+ console.error('Error loading data:', error);
47
+ Swal.fire({
48
+ title: 'Ошибка',
49
+ text: 'Не удалось загрузить данные',
50
+ icon: 'error'
51
+ });
52
+ }
53
+ };
54
+
55
+ const handleLogin = async (e) => {
56
+ e.preventDefault();
57
+ const formData = new FormData(e.target);
58
+ const email = formData.get('email');
59
+ const password = formData.get('password');
60
+
61
+ try {
62
+ const { error } = await supabase.auth.signInWithPassword({
63
+ email,
64
+ password
65
+ });
66
+
67
+ if (error) throw error;
68
+
69
+ setIsLoggedIn(true);
70
+ loadData();
71
+ } catch (error) {
72
+ Swal.fire({
73
+ title: 'Ошибка входа',
74
+ text: error.message,
75
+ icon: 'error'
76
+ });
77
+ }
78
+ };
79
+
80
+ const handleLogout = async () => {
81
+ await supabase.auth.signOut();
82
+ setIsLoggedIn(false);
83
+ };
84
+
85
+ const handleAddEquipment = async (formData) => {
86
+ try {
87
+ const { error } = await supabase.from('equipment').insert([{
88
+ name: formData.get('name'),
89
+ type: formData.get('type'),
90
+ serial: formData.get('serial'),
91
+ inventory: formData.get('inventory'),
92
+ commission_date: formData.get('commissionDate'),
93
+ maintenance_interval: parseInt(formData.get('interval')),
94
+ description: formData.get('description'),
95
+ status: 'active'
96
+ }]);
97
+
98
+ if (error) throw error;
99
+
100
+ loadData();
101
+ Swal.fire({
102
+ title: 'Успешно',
103
+ text: 'Оборудование добавлено',
104
+ icon: 'success'
105
+ });
106
+ } catch (error) {
107
+ Swal.fire({
108
+ title: 'Ошибка',
109
+ text: error.message,
110
+ icon: 'error'
111
+ });
112
+ }
113
+ };
114
+
115
+ if (!isLoggedIn) {
116
+ return (
117
+ <div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center">
118
+ <div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6">
119
+ <h3 className="text-xl font-bold mb-4">Вход в систему</h3>
120
+ <form onSubmit={handleLogin} className="space-y-4">
121
+ <input
122
+ type="email"
123
+ name="email"
124
+ placeholder="Email"
125
+ required
126
+ className="w-full px-4 py-2 border rounded-lg"
127
+ />
128
+ <input
129
+ type="password"
130
+ name="password"
131
+ placeholder="Пароль"
132
+ required
133
+ className="w-full px-4 py-2 border rounded-lg"
134
+ />
135
+ <button
136
+ type="submit"
137
+ className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg"
138
+ >
139
+ Войти
140
+ </button>
141
+ </form>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <div className="flex min-h-screen bg-gray-100">
149
+ {/* Sidebar */}
150
+ <div className={`fixed top-0 left-0 h-full bg-blue-800 text-white ${sidebarCollapsed ? 'w-16' : 'w-64'} p-4 transition-all duration-300`}>
151
+ <div className="flex items-center justify-between mb-6">
152
+ {!sidebarCollapsed && <h1 className="text-xl font-bold">MES System</h1>}
153
+ <button onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="text-white hover:text-blue-200">
154
+ <FontAwesomeIcon icon={faBars} />
155
+ </button>
156
+ </div>
157
+
158
+ <nav>
159
+ <ul className="space-y-2">
160
+ {[
161
+ { id: 'dashboard', icon: faTachometerAlt, label: 'Главная' },
162
+ { id: 'schedule', icon: faCalendarAlt, label: 'График ТО' },
163
+ { id: 'equipment', icon: faTools, label: 'Оборудование' },
164
+ { id: 'maintenance', icon: faClipboardCheck, label: 'Техобслуживание' },
165
+ { id: 'reports', icon: faChartLine, label: 'Отчеты' },
166
+ { id: 'users', icon: faUsersCog, label: 'Пользователи' }
167
+ ].map(item => (
168
+ <li key={item.id}>
169
+ <button
170
+ onClick={() => setCurrentTab(item.id)}
171
+ className={`w-full text-left px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center ${currentTab === item.id ? 'bg-blue-700' : ''}`}
172
+ >
173
+ <FontAwesomeIcon icon={item.icon} className="mr-2" />
174
+ {!sidebarCollapsed && item.label}
175
+ </button>
176
+ </li>
177
+ ))}
178
+ </ul>
179
+ </nav>
180
+ </div>
181
+
182
+ {/* Main Content */}
183
+ <div className={`flex-1 ${sidebarCollapsed ? 'ml-16' : 'ml-64'} p-6 transition-all duration-300`}>
184
+ {/* Header */}
185
+ <header className="bg-white shadow-sm p-4 rounded-lg mb-6 flex justify-between items-center">
186
+ <h1 className="text-2xl font-semibold">
187
+ {currentTab === 'dashboard' ? 'Главная панель' :
188
+ currentTab === 'schedule' ? 'График ТО' :
189
+ currentTab === 'equipment' ? 'Оборудование' :
190
+ currentTab === 'maintenance' ? 'Техобслуживание' :
191
+ currentTab === 'reports' ? 'Отчеты' : 'Пользователи'}
192
+ </h1>
193
+ <div className="flex space-x-4">
194
+ <button className="p-2 rounded-full hover:bg-gray-100 relative">
195
+ <FontAwesomeIcon icon={faBell} className="text-gray-600" />
196
+ <span className="absolute top-0 right-0 bg-red-500 text-white text-xs w-5 h-5 flex items-center justify-center rounded-full">3</span>
197
+ </button>
198
+ <button
199
+ onClick={handleLogout}
200
+ className="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600"
201
+ >
202
+ Выход
203
+ </button>
204
+ </div>
205
+ </header>
206
+
207
+ {/* Content based on current tab */}
208
+ <div className="bg-white rounded-lg shadow p-6">
209
+ {currentTab === 'dashboard' && (
210
+ <Dashboard equipment={equipment} maintenanceEvents={maintenanceEvents} />
211
+ )}
212
+ {currentTab === 'equipment' && (
213
+ <Equipment
214
+ equipment={equipment}
215
+ onAdd={handleAddEquipment}
216
+ onEdit={(item) => {
217
+ // Handle edit
218
+ }}
219
+ onDelete={async (id) => {
220
+ try {
221
+ const { error } = await supabase
222
+ .from('equipment')
223
+ .delete()
224
+ .eq('id', id);
225
+
226
+ if (error) throw error;
227
+
228
+ loadData();
229
+ Swal.fire({
230
+ title: 'Успешно',
231
+ text: 'Оборудование удалено',
232
+ icon: 'success'
233
+ });
234
+ } catch (error) {
235
+ Swal.fire({
236
+ title: 'Ошибка',
237
+ text: error.message,
238
+ icon: 'error'
239
+ });
240
+ }
241
+ }}
242
+ />
243
+ )}
244
+ {/* Add other tab components here */}
245
+ </div>
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ export default App;
src/components/Dashboard.jsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Line, Pie } from 'react-chartjs-2';
3
+ import 'chart.js/auto';
4
+
5
+ function Dashboard({ equipment, maintenanceEvents }) {
6
+ const stats = {
7
+ total: equipment?.length || 0,
8
+ active: equipment?.filter(e => e.status === 'active').length || 0,
9
+ maintenance: equipment?.filter(e => e.status === 'maintenance').length || 0,
10
+ repair: equipment?.filter(e => e.status === 'repair').length || 0
11
+ };
12
+
13
+ const chartData = {
14
+ labels: ['Активно', 'ТО', 'Ремонт'],
15
+ datasets: [{
16
+ data: [stats.active, stats.maintenance, stats.repair],
17
+ backgroundColor: ['#10B981', '#F59E0B', '#EF4444'],
18
+ borderWidth: 0
19
+ }]
20
+ };
21
+
22
+ const chartOptions = {
23
+ responsive: true,
24
+ plugins: {
25
+ legend: {
26
+ position: 'bottom',
27
+ }
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="space-y-6">
33
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
34
+ <div className="bg-white rounded-lg shadow p-6 border-l-4 border-blue-500">
35
+ <h4 className="font-medium text-gray-500">Всего оборудования</h4>
36
+ <p className="text-2xl font-bold">{stats.total}</p>
37
+ </div>
38
+ <div className="bg-white rounded-lg shadow p-6 border-l-4 border-green-500">
39
+ <h4 className="font-medium text-gray-500">Активно</h4>
40
+ <p className="text-2xl font-bold">{stats.active}</p>
41
+ </div>
42
+ <div className="bg-white rounded-lg shadow p-6 border-l-4 border-yellow-500">
43
+ <h4 className="font-medium text-gray-500">На ТО</h4>
44
+ <p className="text-2xl font-bold">{stats.maintenance}</p>
45
+ </div>
46
+ <div className="bg-white rounded-lg shadow p-6 border-l-4 border-red-500">
47
+ <h4 className="font-medium text-gray-500">В ремонте</h4>
48
+ <p className="text-2xl font-bold">{stats.repair}</p>
49
+ </div>
50
+ </div>
51
+
52
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
53
+ <div className="bg-white rounded-lg shadow p-6">
54
+ <h3 className="text-lg font-semibold mb-4">Статус оборудования</h3>
55
+ <div className="h-64">
56
+ <Pie data={chartData} options={chartOptions} />
57
+ </div>
58
+ </div>
59
+ <div className="bg-white rounded-lg shadow p-6">
60
+ <h3 className="text-lg font-semibold mb-4">Последние события</h3>
61
+ <div className="space-y-4">
62
+ {maintenanceEvents?.slice(0, 5).map(event => (
63
+ <div key={event.id} className="flex items-center justify-between border-b pb-2">
64
+ <div>
65
+ <p className="font-medium">{equipment?.find(e => e.id === event.equipment_id)?.name}</p>
66
+ <p className="text-sm text-gray-500">{new Date(event.date).toLocaleDateString()}</p>
67
+ </div>
68
+ <span className={`px-2 py-1 rounded-full text-xs font-semibold ${
69
+ event.status === 'completed' ? 'bg-green-100 text-green-800' :
70
+ event.status === 'planned' ? 'bg-blue-100 text-blue-800' :
71
+ 'bg-yellow-100 text-yellow-800'
72
+ }`}>
73
+ {event.status}
74
+ </span>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export default Dashboard;
src/components/Equipment.jsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
+ import { faEdit, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons';
4
+ import Swal from 'sweetalert2';
5
+
6
+ function Equipment({ equipment, onAdd, onEdit, onDelete }) {
7
+ const [filters, setFilters] = useState({
8
+ status: '',
9
+ type: '',
10
+ search: ''
11
+ });
12
+
13
+ const filteredEquipment = equipment.filter(item => {
14
+ return (
15
+ (!filters.status || item.status === filters.status) &&
16
+ (!filters.type || item.type === filters.type) &&
17
+ (!filters.search ||
18
+ item.name.toLowerCase().includes(filters.search.toLowerCase()) ||
19
+ item.serial.toLowerCase().includes(filters.search.toLowerCase()) ||
20
+ item.inventory.toLowerCase().includes(filters.search.toLowerCase())
21
+ )
22
+ );
23
+ });
24
+
25
+ const handleDelete = (id) => {
26
+ Swal.fire({
27
+ title: 'Подтверждение',
28
+ text: 'Вы уверены, что хотите удалить это оборудование?',
29
+ icon: 'warning',
30
+ showCancelButton: true,
31
+ confirmButtonText: 'Да, удалить',
32
+ cancelButtonText: 'Отмена'
33
+ }).then((result) => {
34
+ if (result.isConfirmed) {
35
+ onDelete(id);
36
+ }
37
+ });
38
+ };
39
+
40
+ return (
41
+ <div>
42
+ <div className="mb-4 flex justify-between items-center">
43
+ <h2 className="text-2xl font-bold">Оборудование</h2>
44
+ <button
45
+ onClick={onAdd}
46
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"
47
+ >
48
+ <FontAwesomeIcon icon={faPlus} className="mr-2" />
49
+ Добавить оборудование
50
+ </button>
51
+ </div>
52
+
53
+ <div className="mb-4 flex space-x-4">
54
+ <select
55
+ value={filters.status}
56
+ onChange={(e) => setFilters({ ...filters, status: e.target.value })}
57
+ className="border rounded-lg px-4 py-2"
58
+ >
59
+ <option value="">Все статусы</option>
60
+ <option value="active">Активно</option>
61
+ <option value="maintenance">ТО</option>
62
+ <option value="repair">Ремонт</option>
63
+ </select>
64
+
65
+ <select
66
+ value={filters.type}
67
+ onChange={(e) => setFilters({ ...filters, type: e.target.value })}
68
+ className="border rounded-lg px-4 py-2"
69
+ >
70
+ <option value="">Все типы</option>
71
+ <option value="Станок">Станок</option>
72
+ <option value="Конвейер">Конвейер</option>
73
+ <option value="Компрессор">Компрессор</option>
74
+ <option value="Генератор">Генератор</option>
75
+ <option value="Насос">Насос</option>
76
+ </select>
77
+
78
+ <input
79
+ type="text"
80
+ value={filters.search}
81
+ onChange={(e) => setFilters({ ...filters, search: e.target.value })}
82
+ placeholder="Поиск..."
83
+ className="border rounded-lg px-4 py-2 flex-1"
84
+ />
85
+ </div>
86
+
87
+ <div className="bg-white rounded-lg shadow overflow-hidden">
88
+ <table className="min-w-full divide-y divide-gray-200">
89
+ <thead className="bg-gray-50">
90
+ <tr>
91
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Название</th>
92
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Тип</th>
93
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Статус</th>
94
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Инв. номер</th>
95
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Действия</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody className="bg-white divide-y divide-gray-200">
99
+ {filteredEquipment.map((item) => (
100
+ <tr key={item.id}>
101
+ <td className="px-6 py-4 whitespace-nowrap">{item.name}</td>
102
+ <td className="px-6 py-4 whitespace-nowrap">{item.type}</td>
103
+ <td className="px-6 py-4 whitespace-nowrap">
104
+ <span className={`px-2 py-1 rounded-full text-xs font-semibold ${
105
+ item.status === 'active' ? 'bg-green-100 text-green-800' :
106
+ item.status === 'maintenance' ? 'bg-yellow-100 text-yellow-800' :
107
+ 'bg-red-100 text-red-800'
108
+ }`}>
109
+ {item.status === 'active' ? 'Активно' :
110
+ item.status === 'maintenance' ? 'ТО' : 'Ремонт'}
111
+ </span>
112
+ </td>
113
+ <td className="px-6 py-4 whitespace-nowrap">{item.inventory}</td>
114
+ <td className="px-6 py-4 whitespace-nowrap">
115
+ <button
116
+ onClick={() => onEdit(item)}
117
+ className="text-blue-600 hover:text-blue-800 mr-3"
118
+ >
119
+ <FontAwesomeIcon icon={faEdit} />
120
+ </button>
121
+ <button
122
+ onClick={() => handleDelete(item.id)}
123
+ className="text-red-600 hover:text-red-800"
124
+ >
125
+ <FontAwesomeIcon icon={faTrash} />
126
+ </button>
127
+ </td>
128
+ </tr>
129
+ ))}
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ export default Equipment;
src/index.css ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7
+ line-height: 1.5;
8
+ font-weight: 400;
9
+ }
10
+
11
+ body {
12
+ margin: 0;
13
+ min-width: 320px;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ .notification-badge {
18
+ position: absolute;
19
+ top: 0;
20
+ right: 0;
21
+ font-size: 0.625rem;
22
+ line-height: 1.25rem;
23
+ width: 1.25rem;
24
+ height: 1.25rem;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ border-radius: 9999px;
29
+ }
30
+
31
+ .has-event:hover {
32
+ background-color: #BFDBFE !important;
33
+ }
src/main.jsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import {
4
+ Chart as ChartJS,
5
+ CategoryScale,
6
+ LinearScale,
7
+ PointElement,
8
+ LineElement,
9
+ Title,
10
+ Tooltip,
11
+ Legend,
12
+ ArcElement
13
+ } from 'chart.js';
14
+ import App from './App';
15
+ import './index.css';
16
+
17
+ // Register Chart.js components
18
+ ChartJS.register(
19
+ CategoryScale,
20
+ LinearScale,
21
+ PointElement,
22
+ LineElement,
23
+ Title,
24
+ Tooltip,
25
+ Legend,
26
+ ArcElement
27
+ );
28
+
29
+ ReactDOM.createRoot(document.getElementById('root')).render(
30
+ <React.StrictMode>
31
+ <App />
32
+ </React.StrictMode>
33
+ );
supabase/migrations/20250518080628_heavy_mountain.sql ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ # Initial MES System Schema
3
+
4
+ 1. New Tables
5
+ - `equipment`
6
+ - `id` (uuid, primary key)
7
+ - `name` (text)
8
+ - `type` (text)
9
+ - `serial` (text)
10
+ - `inventory` (text)
11
+ - `commission_date` (date)
12
+ - `maintenance_interval` (integer)
13
+ - `description` (text)
14
+ - `status` (text)
15
+ - `created_at` (timestamptz)
16
+ - `updated_at` (timestamptz)
17
+
18
+ - `maintenance_events`
19
+ - `id` (uuid, primary key)
20
+ - `equipment_id` (uuid, foreign key)
21
+ - `date` (date)
22
+ - `status` (text)
23
+ - `responsible` (uuid, foreign key)
24
+ - `notes` (text)
25
+ - `created_at` (timestamptz)
26
+
27
+ 2. Security
28
+ - Enable RLS on all tables
29
+ - Add policies for authenticated users
30
+ */
31
+
32
+ -- Create equipment table
33
+ CREATE TABLE IF NOT EXISTS equipment (
34
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
35
+ name text NOT NULL,
36
+ type text NOT NULL,
37
+ serial text NOT NULL,
38
+ inventory text NOT NULL,
39
+ commission_date date NOT NULL,
40
+ maintenance_interval integer NOT NULL,
41
+ description text,
42
+ status text NOT NULL DEFAULT 'active',
43
+ created_at timestamptz DEFAULT now(),
44
+ updated_at timestamptz DEFAULT now()
45
+ );
46
+
47
+ -- Create maintenance_events table
48
+ CREATE TABLE IF NOT EXISTS maintenance_events (
49
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
50
+ equipment_id uuid REFERENCES equipment(id) ON DELETE CASCADE,
51
+ date date NOT NULL,
52
+ status text NOT NULL,
53
+ responsible uuid REFERENCES auth.users(id),
54
+ notes text,
55
+ created_at timestamptz DEFAULT now()
56
+ );
57
+
58
+ -- Enable RLS
59
+ ALTER TABLE equipment ENABLE ROW LEVEL SECURITY;
60
+ ALTER TABLE maintenance_events ENABLE ROW LEVEL SECURITY;
61
+
62
+ -- Create policies
63
+ CREATE POLICY "Allow authenticated users to read equipment"
64
+ ON equipment
65
+ FOR SELECT
66
+ TO authenticated
67
+ USING (true);
68
+
69
+ CREATE POLICY "Allow authenticated users to insert equipment"
70
+ ON equipment
71
+ FOR INSERT
72
+ TO authenticated
73
+ WITH CHECK (true);
74
+
75
+ CREATE POLICY "Allow authenticated users to update equipment"
76
+ ON equipment
77
+ FOR UPDATE
78
+ TO authenticated
79
+ USING (true);
80
+
81
+ CREATE POLICY "Allow authenticated users to read maintenance events"
82
+ ON maintenance_events
83
+ FOR SELECT
84
+ TO authenticated
85
+ USING (true);
86
+
87
+ CREATE POLICY "Allow authenticated users to insert maintenance events"
88
+ ON maintenance_events
89
+ FOR INSERT
90
+ TO authenticated
91
+ WITH CHECK (true);
92
+
93
+ -- Create updated_at trigger
94
+ CREATE OR REPLACE FUNCTION update_updated_at()
95
+ RETURNS TRIGGER AS $$
96
+ BEGIN
97
+ NEW.updated_at = now();
98
+ RETURN NEW;
99
+ END;
100
+ $$ LANGUAGE plpgsql;
101
+
102
+ CREATE TRIGGER update_equipment_updated_at
103
+ BEFORE UPDATE ON equipment
104
+ FOR EACH ROW
105
+ EXECUTE FUNCTION update_updated_at();
tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
vite.config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ envDir: './',
7
+ envPrefix: 'VITE_'
8
+ })