1
0
Fork 0
This commit is contained in:
Layfer7 2023-12-24 13:38:47 +02:00
commit 9e8fd11cca
16 changed files with 477 additions and 70 deletions

View File

@ -317,12 +317,12 @@
}
],
"wagonTypeList": [
"HalfCarriage",
"Platform",
"CoveredCarriage",
"UsoPlatform",
"Hopper",
"Tank"
["HalfCarriage","Полувагон"],
["Platform","Платформа"],
["CoveredCarriage","Крытый вагон"],
["UsoPlatform","Платформа УСО"],
["Hopper","Хоппер"],
["Tank","Цистерна"]
],
"ownersList": [
"НТС",

View File

@ -7,7 +7,7 @@ import prittierConfig from 'eslint-plugin-prettier/recommended.js';
import react from 'eslint-plugin-react';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
/** @type { import("eslint").Linter.FlatConfig[] } */
export default [
{
@ -52,11 +52,20 @@ export default [
'import/no-named-as-default-member': 'warn',
'import/no-duplicates': 'warn',
'@typescript-eslint/no-unused-vars': 'off',
'unicorn/filename-case': [
'error',
{
case: 'camelCase',
},
],
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
},
plugins: {
'@typescript-eslint': typescript,
react,
import: importPlugin,
'react-hooks': reactHooksPlugin,
},
},
importPlugin.configs.typescript,

View File

@ -22,7 +22,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@remix-run/dev": "^2.4.0",
"@remix-run/dev": "^2.4.1",
"@types/bcrypt": "^5.0.2",
"@types/eslint": "^8.56.0",
"@types/lodash": "^4.14.202",
@ -36,6 +36,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unicorn": "github:sindresorhus/eslint-plugin-unicorn",
"lodash": "^4.17.21",
"prettier": "3.1.1",
@ -45,9 +46,10 @@
},
"dependencies": {
"@nextui-org/react": "^2.2.9",
"@remix-run/node": "^2.4.0",
"@remix-run/react": "^2.4.0",
"@remix-run/serve": "^2.4.0",
"@reduxjs/toolkit": "^2.0.1",
"@remix-run/node": "^2.4.1",
"@remix-run/react": "^2.4.1",
"@remix-run/serve": "^2.4.1",
"bcrypt": "^5.1.1",
"framer-motion": "^10.16.16",
"isbot": "^3.7.1",
@ -55,6 +57,8 @@
"node-fetch": "^3.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.0.4",
"redux": "^5.0.1",
"remix-auth": "^3.6.0",
"remix-auth-form": "^1.4.0"
}

View File

@ -1,6 +1,6 @@
import type { IStation } from 'app/services/api/interfaces.server';
import React from 'react';
import {Select, SelectSection, SelectItem} from "@nextui-org/react";
import { Select, SelectSection, SelectItem } from '@nextui-org/react';
export default function ControlFormHeader({stations}: {stations: IStation[]}) {
return(
@ -14,10 +14,10 @@ export default function ControlFormHeader({stations}: {stations: IStation[]}) {
>
{stations.map((station) => (
<SelectItem key={station.title} value={station.title}>
{station.title}
{station.title}
</SelectItem>
))}
</Select>
</div>
)
);
}

View File

@ -1,9 +0,0 @@
import React from 'react';
export default function controlTable() {
return(
<div id="control-table">
</div>
)
}

View File

@ -0,0 +1,307 @@
import React from 'react';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Button,
useDisclosure,
Listbox,
ListboxItem,
Select,
SelectItem,
} from '@nextui-org/react';
import { IStationData } from 'app/services/api/interfaces.server';
import { useSearchParams, useNavigate } from '@remix-run/react';
type Tdb = {
stationsData: IStationData[];
ownersList: string[];
wagonTypeList: string[][];
};
type Tfilters = {
[filterKey: string]: {
title: string;
items: { key: string; value: string }[];
};
};
const filterMenuKeys: Record<string, string> = {
wagons: 'Вагоны',
ways: 'Ж/Д Пути',
stations: 'Станции',
};
function getFilterSelectedKeys(searchParameters: URLSearchParams) {
const result: {
[filterMenuKey: string]: { [filterKey: string]: Set<string> };
} = {};
const _filters = [...searchParameters.entries()]
.filter((parameter) => parameter[0].startsWith('filter'))
.map((parameter) => [parameter[0].replace('filter', ''), parameter[1]]);
for (const [name, value] of _filters) {
const [filterMenuKey, filterKey] = name
.match(/[A-Z][a-z]*/g)!
.map((value_) => value_.toLowerCase());
if (!(filterMenuKey in result)) {
result[filterMenuKey] = {};
}
result[filterMenuKey][filterKey] = new Set(value.split(','));
}
return result;
}
export function FilterMenu({
activeFilterMenuKey,
filters,
statesSelectedKeys,
}: {
activeFilterMenuKey: string;
filters: Tfilters;
statesSelectedKeys: {
[activeFilterMenuKey: string]: {
[filterKey: string]: [
Set<string>,
React.Dispatch<React.SetStateAction<Set<string>>>,
];
};
};
}) {
const [searchParameters] = useSearchParams();
const _t = getFilterSelectedKeys(searchParameters);
for (const filterKey of Object.keys(filters)) {
if (
!(
filterKey in
Object.keys(statesSelectedKeys[activeFilterMenuKey])
) &&
!(activeFilterMenuKey in _t)
) {
_t[activeFilterMenuKey] = {};
}
}
return (
<>
{Object.keys(filters).map((filterKey) => {
return (
<>
<h4>{filters[filterKey].title}</h4>
<Select
aria-label="Один из пунктов фильтра"
placeholder="Не выбрано"
selectionMode="multiple"
onChange={(event)=> {
statesSelectedKeys[activeFilterMenuKey][
filterKey
][1](new Set(event.target.value.split(",")))
}}
selectedKeys={
statesSelectedKeys[activeFilterMenuKey][
filterKey
][0]
}
onSelectionChange={(keys) =>
(keys as Set<string>)
}
>
{filters[filterKey].items.map((value) => {
return (
<SelectItem
key={value.key}
value={value.value}
>
{value.key}
</SelectItem>
);
})}
</Select>
</>
);
})}
</>
);
}
export default function ModalFilter({ db }: { db: Tdb }) {
const filters: { [filterMenuKey: string]: Tfilters } = {
wagons: {
owner: {
title: 'Собственник',
items: db.ownersList.map((owner) => {
return { key: owner, value: owner };
}),
},
state: {
title: 'Состояние вагона',
items: [
{
key: 'Не прибыл',
value: 'NotArrived',
},
{
key: 'Простаивает',
value: 'StandingBy',
},
{
key: 'Закончил',
value: 'Finished',
},
{
key: 'В работе',
value: 'inProgress',
},
],
},
type: {
title: 'Тип вагона',
items: db.wagonTypeList.map((type) => {
return { key: type[1], value: type[0] };
}),
},
},
ways: {
empty: {
title: 'Загруженность пути',
items: [
{
key: 'Полная',
value: 'full',
},
{
key: 'Частичная',
value: 'half',
},
{
key: 'Отсутствует',
value: 'empty',
},
],
},
},
};
const [searchParameters] = useSearchParams();
const navigate = useNavigate();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [activeFilterMenuKey, setActiveFilterMenuKey] = React.useState(
Object.keys(filterMenuKeys)[0],
);
const statesSelectedKeys: {
[activeFilterMenuKey: string]: {
[filterKey: string]: [
Set<string>,
React.Dispatch<React.SetStateAction<Set<string>>>,
];
};
} = {};
statesSelectedKeys[activeFilterMenuKey] = {};
const _t = getFilterSelectedKeys(searchParameters);
for (const filterMenuKey in filters) {
if (!(filterMenuKey in statesSelectedKeys)) {
statesSelectedKeys[filterMenuKey] = {};
}
for (const filterKey in filters[filterMenuKey]) {
/* eslint-disable-next-line */
statesSelectedKeys[filterMenuKey][filterKey] = React.useState(
_t[filterMenuKey] === undefined
? new Set()
: _t[filterMenuKey][filterKey] ?? new Set(),
);
}
}
return (
<div className="mb-4">
<Button onPress={onOpen}>Фильтр</Button>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
scrollBehavior="outside"
size="xl"
>
<ModalContent aria-label="Пользовательский фильтр">
{(onClose) => (
<>
<ModalHeader>Пользовательский фильтр</ModalHeader>
<ModalBody>
<Listbox
onAction={(menuKey) =>
setActiveFilterMenuKey(
menuKey.toString(),
)
}
>
{Object.keys(filterMenuKeys).map((key) => (
<ListboxItem key={key}>
{filterMenuKeys[key]}
</ListboxItem>
))}
</Listbox>
<div>
<FilterMenu
activeFilterMenuKey={
activeFilterMenuKey
}
filters={filters[activeFilterMenuKey]}
statesSelectedKeys={statesSelectedKeys}
/>
</div>
<Button
onClick={() => {
for (const menuKey in statesSelectedKeys) {
for (const filterKey in statesSelectedKeys[
menuKey
]) {
const _m=Array.from(
statesSelectedKeys[
menuKey
][
filterKey
][0].values()
)
if (
_m[0] === '' || _m.length==0
) {
searchParameters.delete(
`filter${
menuKey[0].toUpperCase() +
menuKey.slice(1)
}${
filterKey[0].toUpperCase() +
filterKey.slice(1)
}`,
);
} else {
searchParameters.set(
`filter${
menuKey[0].toUpperCase() +
menuKey.slice(1)
}${
filterKey[0].toUpperCase() +
filterKey.slice(1)
}`,
_m.join(','),
);
}
}
}
navigate(
'?' + searchParameters.toString(),
);
onClose();
}}
>
Применить
</Button>
<Button onClick={onClose}>Отмена</Button>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</div>
);
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Button,
useDisclosure,
Listbox,
ListboxItem,
Select,
SelectItem,
ModalFooter,
} from '@nextui-org/react';
import { IStationData } from 'app/services/api/interfaces.server';
import { useSearchParams, useNavigate } from '@remix-run/react';
export default function ModalOperations () {
const {isOpen, onOpen, onOpenChange} = useDisclosure();
return (
<>
<Button onPress={onOpen}>Операции</Button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">Операции</ModalHeader>
<ModalBody className="w-[80%]">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam pulvinar risus non risus hendrerit venenatis.
Pellentesque sit amet hendrerit risus, sed porttitor quam.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam pulvinar risus non risus hendrerit venenatis.
Pellentesque sit amet hendrerit risus, sed porttitor quam.
</p>
<p>
Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit
dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis.
Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod.
Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur
proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam.
</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Action
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}

View File

@ -56,32 +56,10 @@ export default function NavbarCom() {
Powered by EVRAZ
</p>
</NavbarBrand>
<NavbarItem className="mx-auto">
<Link color="foreground" to="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link to="#" aria-current="page">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" to="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link to="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" to="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
<NavbarMenu>

View File

@ -4,6 +4,7 @@ import Wagon from './wagon';
import Locomotive from './locomotive';
export default function Park({park, railRoads}: {park: IPark, railRoads: IStationData[]}) {
return(
<div id="control-park" className='relative mb-12 py-1 pl-8 pr-2 border-secondary-200 border-[1px]'>
<div className="absolute left-0 top-0 w-6 h-full bg-secondary-200 flex justify-center items-center" style={{writingMode: "vertical-lr"}}>{park.name}</div>

View File

@ -1,8 +1,8 @@
import React from 'react';
import React, { useContext } from 'react';
import { IWagon } from 'app/services/api/interfaces.server';
import { useSelector, useDispatch } from 'react-redux'
import { selectWagon } from 'app/wagonsSlice';
export default function Wagon({ wagon }: { wagon: IWagon }) {
console.log(wagon)
const colors: Record<string, string[]> = {
НТС: ['#BCF3FF', '#2988AE'],
ГК: ['#C8F4C1', '#6EA566'],
@ -27,8 +27,16 @@ export default function Wagon({ wagon }: { wagon: IWagon }) {
bottomFillColor = '#EB5835';
bottomStrokeColor = '#E32112';
}
const wagons = useSelector(selectWagon);
const dispatch = useDispatch();
const selectWagonHandle = (e: any) => {
const id = e.currentTarget.getAttribute("wagon-id");
}
return (
<div id="wagon" inventory-data={wagon.id} onClick={e => console.log(e.currentTarget)} className=" w-fit relative flex flex-col items-center">
<div id="wagon" wagon-id={wagon.id} onClick={selectWagonHandle} className=" w-fit relative flex flex-col items-center">
<div className="relative top-[2px]">
{WagonTop(wagon.type, topFillColor, topBorderColor)}
</div>

View File

@ -1,31 +1,48 @@
import React from 'react';
import React, { useState } from 'react';
import { createContext, useContext } from 'react';
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { authenticator } from '../services/auth.server';
import { IStation, IStationData } from 'app/services/api/interfaces.server';
import { api } from 'app/services/api/json.api.server';
import { useLoaderData } from '@remix-run/react';
import ControlFormHeader from 'app/coms/controlFormHeader';
import ModalFilter from 'app/coms/modalFilter';
import Station from 'app/coms/ui/Station';
import { Input, Table, TableColumn, TableHeader } from '@nextui-org/react';
import { Provider } from 'react-redux'
import { store } from 'app/wagonsSlice';
import ModalOperations from 'app/coms/modalOperations';
export async function loader({ request }: LoaderFunctionArgs) {
const authData = await authenticator.isAuthenticated(request, {
failureRedirect: '/login',
});
const stations: IStation[] = await api.getStations();
const stationsData = await api.getStationsData();
const owners: string[] = await api.getOwners();
const stationsData: IStationData[] = await api.getStationsData();
const wagonTypeList = await api.getWagonTypes();
return json({
stations,
stationsData,
owners,
authData,
wagonTypeList,
});
}
export default function Index() {
const {authData, stations, stationsData, owners} = useLoaderData<typeof loader>();
const { authData, stations, owners, stationsData, wagonTypeList } =
useLoaderData<typeof loader>();
return (
<div id="index" className='w-full h-full'>
<div className="flex justify-between">
<ModalFilter
db={{
stationsData: stationsData,
ownersList: owners,
wagonTypeList,
}}
/>
<ModalOperations />
</div>
<div id="control-form" className='w-full h-full flex flex-col rounded-sm border-secondary-200 border-[1px]'>
<div className="h-9 mb-4 pl-4 bg-secondary-100 text-y-center rounded-t-sm flex items-center">
АРМ дежурного станции
@ -48,13 +65,14 @@ export default function Index() {
<div className="w-12 flex justify-center items-center ">
Л
</div>
</div>
{stations.map(station => {
const stationData = stationsData.filter(el => el.station.id == station.id);
return(<Station stationData={stationData} station={station} key={station.id}/>)
})
}
</div>
</div>
<Provider store={store}>
{stations.map(station => {
const stationData = stationsData.filter(el => el.station.id == station.id);
return(<Station stationData={stationData} station={station} key={station.id}/>)
})}
</Provider>
</div>
</div>
);
}

View File

@ -28,7 +28,6 @@ export default function Auth() {
placeholder="Enter your password"
/>
<Button type="submit" color="primary">
{' '}
Login
</Button>
</div>

View File

@ -28,7 +28,6 @@ export default function Login() {
placeholder="Enter your password"
/>
<Button type="submit" color="primary">
{' '}
Login
</Button>
</div>

View File

@ -1,6 +1,12 @@
import fetch from 'node-fetch';
import { env } from 'node:process';
import { INorm, IOperationType, IStation, IStationData, IUser } from './interfaces.server';
import {
INorm,
IOperationType,
IStation,
IStationData,
IUser,
} from './interfaces.server';
const endpoints = {
getStations: 'stationsList',
@ -37,7 +43,7 @@ export const api = new (class ApiDB {
const response = await fetch(this.url + endpoints.getWagonTypeList, {
method: 'GET',
});
return (await response.json()) as string[];
return (await response.json()) as string[][];
}
async getOwners() {
const response = await fetch(this.url + endpoints.getOwnersList, {
@ -46,13 +52,19 @@ export const api = new (class ApiDB {
return (await response.json()) as string[];
}
async getStationData(stationID: number): Promise<IStationData[]> {
async getStationData(stationID: number): Promise<IStationData> {
const response = await fetch(
this.url + endpoints.getStationsData + `?station.id=${stationID}`,
{
method: 'GET',
},
);
return ((await response.json()) as IStationData[])[0];
}
async getStationsData(): Promise<IStationData[]> {
const response = await fetch(this.url + endpoints.getStationsData, {
method: 'GET',
});
return (await response.json()) as IStationData[];
}
async getStationsData(): Promise<IStationData[]> {

View File

@ -13,7 +13,6 @@ authenticator.use(
let login = <string>form.get('login');
let password = <string>form.get('password');
const users = await api.getUsers();
for (const user of users) {
if (
user.login == login &&
@ -33,7 +32,7 @@ authenticator.use(
let password = <string>form.get('password');
let hashedPassword = '';
await hash(password, saltRounds, (error, hash) => {
hash(password, saltRounds, (error, hash) => {
if (error) {
throw error;
}

21
src/app/wagonsSlice.ts Normal file
View File

@ -0,0 +1,21 @@
import { createSlice, configureStore } from '@reduxjs/toolkit'
const wagonSlice = createSlice({
name: 'wagonsSlice',
initialState: {
selectedWagons: []
},
reducers: {
selectWagon: (state, action) => {
state.selectedWagons.find(action.payload) ?
state.selectedWagons = state.selectedWagons.filter(el => el == action.payload):
state.selectedWagons = <never[]>[...state.selectedWagons, action.payload]
},
}
})
export const { selectWagon } = wagonSlice.actions
export const store = configureStore({
reducer: wagonSlice.reducer
})