1
0
Fork 0

Fixed somethind and added modal filtering

This commit is contained in:
minicx 2023-12-24 08:03:11 +03:00
commit 96a3c8e40b
16 changed files with 425 additions and 77 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ src/database/prisma/client
*.sqlite
package-lock.json
.vscode
*.db*
*.db*
db.json

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/node": "^18.19.3",
@ -35,6 +35,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",
@ -44,9 +45,9 @@
},
"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",
"@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",

View File

@ -1,23 +1,27 @@
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(
export default function ControlFormHeader({
Stations,
}: {
Stations: IStation[];
}) {
return (
<div id="control-form-header" className="flex gap-4">
<Select
label="Station filter"
placeholder="Select a station"
selectionMode="multiple"
className="w-[250px]"
size='sm'
>
size="sm"
>
{Stations.map((station) => (
<SelectItem key={station.title} value={station.title}>
{station.title}
{station.title}
</SelectItem>
))}
</Select>
</div>
)
);
}

View File

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

View File

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

View File

@ -1,10 +0,0 @@
import type { IStationData } from 'app/services/api/interfaces.server';
import React from 'react';
export default function Station({StationData}: {StationData: IStationData}) {
return(
<div id="control-station">
</div>
)
}

5
src/app/coms/ui/park.tsx Normal file
View File

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

View File

@ -0,0 +1,10 @@
import type { IStationData } from 'app/services/api/interfaces.server';
import React from 'react';
export default function Station({
StationData,
}: {
StationData: IStationData;
}) {
return <div id="control-station"></div>;
}

View File

@ -1,30 +1,42 @@
import React from 'react';
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { authenticator } from '../services/auth.server';
import { IStation } from 'app/services/api/interfaces.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';
export async function loader({ request }: LoaderFunctionArgs) {
const authData = await authenticator.isAuthenticated(request, {
failureRedirect: '/login',
});
const Stations: IStation[] = await api.getStations();
const Owners: string[] = await api.getOwners();
const stations: IStation[] = await api.getStations();
const stationsData = await api.getStationsData();
const owners: string[] = await api.getOwners();
const wagonTypeList = await api.getWagonTypes();
return json({
Stations,
Owners,
stations,
stationsData,
owners,
authData,
wagonTypeList,
});
}
export default function Index() {
const {authData, Stations, Owners} = useLoaderData<typeof loader>();
console.log(Owners);
const { authData, stations, owners, stationsData, wagonTypeList } =
useLoaderData<typeof loader>();
return (
<div id="control-form" className='w-full h-full p-2 flex flex-col'>
<ControlFormHeader Stations={Stations}/>
<div id="control-form" className="w-full h-full p-2 flex flex-col">
<ControlFormHeader Stations={stations} />
<ModalFilter
db={{
stationsData: stationsData,
ownersList: owners,
wagonTypeList,
}}
></ModalFilter>
</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',
@ -9,11 +15,13 @@ const endpoints = {
getOwnersList: 'ownersList',
getNorms: 'operationsTypesNorms',
getOperationsTypes: 'operationsTypes',
getUsers:'users'
getUsers: 'users',
};
export const api = new (class ApiDB {
private readonly url = `http://localhost:${(env.DB_PORT ?? 4000).toString()}/`;
private readonly url = `http://localhost:${(
env.DB_PORT ?? 4000
).toString()}/`;
constructor() {}
async getStations(): Promise<IStation[]> {
const response = await fetch(this.url + endpoints.getStations, {
@ -37,7 +45,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,23 +54,39 @@ 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 getUsers(){
const response = await fetch(this.url + endpoints.getUsers,{method:"GET"});
async getUsers() {
const response = await fetch(this.url + endpoints.getUsers, {
method: 'GET',
});
return (await response.json()) as IUser[];
}
async createUser(user:Omit<IUser,"id">){
const response = await fetch(this.url + endpoints.getUsers,{method:"POST",headers:{
"Content-Type": "application/json",
},body:JSON.stringify(user)});
return response.ok
async createUser(user: Omit<IUser, 'id'>) {
const response = await fetch(this.url + endpoints.getUsers, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
});
if (response.ok) {
return (await response.json()) as IUser;
} else {
throw new Error('Unknown answer from db');
}
}
})();

View File

@ -5,7 +5,6 @@ import { hash, compare } from 'bcrypt';
import { IUser } from './api/interfaces.server';
import { api } from './api/json.api.server';
export let authenticator = new Authenticator<IUser>(sessionStorage);
const saltRounds = 9;
@ -13,26 +12,27 @@ authenticator.use(
new FormStrategy(async ({ form }) => {
let login = <string>form.get('login');
let password = <string>form.get('password');
const users=await api.getUsers();
const users = await api.getUsers();
for (const user of users) {
if (user.login==login && await compare(password,user.password)){
if (
user.login == login &&
(await compare(password, user.password))
) {
return user;
}
}
throw new Error("Wrong credentials")
throw new Error('Wrong credentials');
}),
'user-login',
);
authenticator.use(
new FormStrategy(async ({ form, context }) => {
new FormStrategy(async ({ form }) => {
let login = <string>form.get('login');
let password = <string>form.get('password');
let hashedPassword = '';
await hash(password, saltRounds, (error, hash) => {
hash(password, saltRounds, (error, hash) => {
if (error) {
throw error;
}
@ -42,8 +42,8 @@ authenticator.use(
if (!hashedPassword) {
throw '';
}
const user = {id:4, login: 'null', password: 'null' };
const user = await api.createUser({ login, password: hashedPassword });
return user;
}),