add user selecting

feature/component
Ernest Litvinenko 2023-11-15 14:28:43 +03:00
parent 67ca646800
commit ce70aa6250
11 changed files with 493 additions and 59 deletions

View File

@ -21,6 +21,7 @@
"vite": "^4.4.5"
},
"dependencies": {
"@types/axios": "^0.14.0",
"@types/react": "^18.2.33",
"axios": "^1.6.0",
"flexsearch": "^0.7.31",

52
src/backend/index.ts Normal file
View File

@ -0,0 +1,52 @@
import axios, {AxiosInstance} from "axios";
import {ListOfficesResponse, UpdateOfficeRequest, ListProfilesResponse} from "./types.ts";
class Backend {
instance: AxiosInstance
office: Office
profile: Profile
constructor() {
this.instance = axios.create({
baseURL: 'http://localhost/api/v2/',
headers: {
'Content-Type': 'application/json'
}
})
this.office = new Office({instance: this.instance})
this.profile = new Profile({instance: this.instance})
}
}
class BaseHandler {
instance: AxiosInstance
constructor({instance}: { instance: AxiosInstance }) {
this.instance = instance
}
}
class Office extends BaseHandler {
async list_offices() {
const {data} = await this.instance.get<ListOfficesResponse>('offices')
return data
}
async update_office(office: UpdateOfficeRequest) {
await this.instance.post('offices/', office)
}
}
class Profile extends BaseHandler {
async listProfiles(): Promise<ListProfilesResponse[]> {
const {data} = await this.instance.get<ListProfilesResponse[]>('profiles')
return data
}
async getById(idx: string | number): Promise<ListProfilesResponse> {
const {data} = await this.instance.get<ListProfilesResponse>(`profile/${idx}`)
return data
}
}
export default new Backend()

261
src/backend/openapi.d.ts vendored Normal file
View File

@ -0,0 +1,261 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/api/v2/offices/": {
/** List Offices */
get: operations["list_offices_api_v2_offices__get"];
/** Update Office */
post: operations["update_office_api_v2_offices__post"];
};
"/api/v2/profiles/": {
/** List Profiles */
get: operations["list_profiles_api_v2_profiles__get"];
/** Create Profile */
post: operations["create_profile_api_v2_profiles__post"];
};
"/api/v2/profiles/{profile_id}": {
/** Get Profile */
get: operations["get_profile_api_v2_profiles__profile_id__get"];
/** Update Profile */
put: operations["update_profile_api_v2_profiles__profile_id__put"];
/** Delete Profile */
delete: operations["delete_profile_api_v2_profiles__profile_id__delete"];
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** ExtendedResponse */
ExtendedResponse: {
/** Features */
features: string | null;
contact_person: components["schemas"]["ProfileResponse"] | null;
/** Person Count */
person_count: number | null;
/** Rating */
rating: number | null;
};
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
detail?: components["schemas"]["ValidationError"][];
};
/** JdeOfficeDetailResponse */
JdeOfficeDetailResponse: {
/** Code */
code: string;
/** Title */
title: string;
/** Kladr Code */
kladr_code: string;
/** Aex Only */
aex_only: string;
/** Mst Pr Aex */
mst_pr_aex: string;
/** Mst Pr Virt */
mst_pr_virt: string;
/** Addr */
addr: string;
/** Features */
features: string;
/** Coords */
coords: {
[key: string]: string;
};
/** City */
city: string;
/** Country Code */
country_code: string;
/** Contry Name */
contry_name: string;
/** Max Ves */
max_ves: string;
/** Max Obyom */
max_obyom: string;
/** Max Ves Gm */
max_ves_gm: string;
/** Max Obyom Gm */
max_obyom_gm: string;
/** Max L Gm */
max_l_gm: string;
/** Max W Gm */
max_w_gm: string;
/** Max H Gm */
max_h_gm: string;
changeable_info: components["schemas"]["ExtendedResponse"] | null;
};
/** ProfileResponse */
ProfileResponse: {
/** Id */
id: number;
/** Full Name */
full_name: string | null;
/** Phone */
phone: string | null;
/** Email */
email: string | null;
};
/** UpdateOfficeRequest */
UpdateOfficeRequest: {
/** Code */
code: number;
/** Features */
features: string | null;
/** Contact Person Id */
contact_person_id: number | null;
/** Person Count */
person_count: number | null;
/** Rating */
rating: number | null;
};
/** ValidationError */
ValidationError: {
/** Location */
loc: (string | number)[];
/** Message */
msg: string;
/** Error Type */
type: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type external = Record<string, never>;
export interface operations {
/** List Offices */
list_offices_api_v2_offices__get: {
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["JdeOfficeDetailResponse"][];
};
};
};
};
/** Update Office */
update_office_api_v2_offices__post: {
requestBody: {
content: {
"application/json": components["schemas"]["UpdateOfficeRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** List Profiles */
list_profiles_api_v2_profiles__get: {
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
};
};
/** Create Profile */
create_profile_api_v2_profiles__post: {
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
};
};
/** Get Profile */
get_profile_api_v2_profiles__profile_id__get: {
parameters: {
path: {
profile_id: number;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** Update Profile */
update_profile_api_v2_profiles__profile_id__put: {
parameters: {
path: {
profile_id: number;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** Delete Profile */
delete_profile_api_v2_profiles__profile_id__delete: {
parameters: {
path: {
profile_id: number;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}

File diff suppressed because one or more lines are too long

15
src/backend/types.ts Normal file
View File

@ -0,0 +1,15 @@
import types from './openapi'
// Responses
export type ListOfficesResponse = types.paths['/api/v2/offices/']['get']['responses'][200]['content']['application/json']
export type ListProfilesResponse = {
id: number,
full_name : string | null,
phone: string | null,
email: string | null
}
//Requests
export type UpdateOfficeRequest = types.paths['/api/v2/offices/']['post']['requestBody']['content']['application/json']

3
src/helplers.ts Normal file
View File

@ -0,0 +1,3 @@
export function gen_unique_id(): string {
return String(Date.now())
}

View File

@ -1,13 +1,12 @@
import './style.css'
import './fonts/ManiaExtended/stylesheet.css'
import './fonts/OfficinaSerifBoldSCC/stylesheet.css'
import axios from "axios";
import $ from 'jquery';
import _ from 'lodash';
import FlexSearch from 'flexsearch';
import {BlocksDataValueType, FilterType, ServerResponseType} from "./types.ts";
import {BlocksDataValueType, FilterType} from "./types.ts";
import Block, {toggleBlocks} from "./block.ts";
import {renderModal} from "./modal.ts";
import Backend from "./backend";
// use this for search
@ -38,10 +37,7 @@ let blocksData: { [x: string]: BlocksDataValueType } = {}
// Containers
const dataContainer = $('#data')
const menuContainer = $('#filterForm')
const getDataFromServer = async () => {
const r = await axios.get<ServerResponseType[]>('http://localhost/api/v2/offices')
return r.data
}
const updateDisplayingData = (...ids: Array<string | number>) => {
console.log(ids)
@ -116,7 +112,7 @@ const reRenderHandler = () => {
dataContainer.children().remove()
index = new Search()
getDataFromServer().then(
Backend.office.list_offices().then(
d => {
d.forEach(
data => {

View File

@ -1,5 +1,6 @@
import axios from "axios";
import $ from 'jquery';
import Backend from "./backend";
import {UpdateOfficeRequest} from './backend/types.ts'
type ModalPropsType = {
title: string
@ -15,35 +16,104 @@ type ModalPropsType = {
personalCount?: number
}
let selectedValueUser = 0;
let selectedUser = {
id: "",
full_name: "",
email: "",
phone: ""
}
const updateCurrentUser = new Proxy({html: ``}, {
set(target, p: string, newValue: any): boolean {
target[p] = newValue
return true
},
get(target, prop) {
if (prop === 'html') {
Backend.profile.listProfiles()
.then(data => data.map(
elem => `<option value="${elem.id}">${elem.full_name}</option>`
))
.then(data => `<select name="selectUser">${data.join('')}</select>`)
.then(data => updateCurrentUser.html = data)
return target.html
}
}
})
const UserCreationBlock = (props: ModalPropsType) => {
const createUser = `
<div>
<input class="border-2 border-secondary flex-1" type="text" name="profileFullName" placeholder="ФИО" value=""/>
<input class="border-2 border-secondary flex-1" type="email" name="profileEmail" placeholder="Email" />
<input class="border-2 border-secondary flex-1" type="email" name="profilePhone" placeholder="+79999999999" />
</div>
`
return $(createUser)
}
const UserSelectingBlock = (props: ModalPropsType) => {
console.log('user id: ', selectedUser.id)
if (selectedUser.id === '') {
selectedUser.id = props.contact.id ? String(props.contact.id) : ''
selectedUser.phone = props.contact.phone || ''
selectedUser.email = props.contact.email || ''
selectedUser.full_name = props.contact.fullName || ''
}
console.log(selectedUser)
const query = `
<div>
<select name="profileId" id="selectUserField">
</select>
<input class="border-2 border-secondary flex-1" type="text" name="profileFullName" placeholder="ФИО" value="${selectedUser.full_name}"/>
<input class="border-2 border-secondary flex-1" type="email" name="profileEmail" placeholder="Email" value="${selectedUser.email}"/>
<input class="border-2 border-secondary flex-1" type="email" name="profilePhone" placeholder="+79999999999" value="${selectedUser.phone}" />
</div>
`
const queryBlock = $(query)
Backend.profile.listProfiles()
.then(data => data.map(elem => `<option value="${elem.id}">${elem.full_name}</option>`).join(""))
.then(e => queryBlock.find('select').append(e))
return queryBlock
}
const Modal = (props: ModalPropsType) => `
<div class="fixed top-0 left-0 w-full h-full bg-secondary bg-opacity-50 z-10 flex flex-col justify-center" id="modal">
<button class="text-white absolute top-14 right-14" id="removeModal">x</button>
<div class="container flex flex-col mx-auto bg-white px-4 py-2">
<span class="text-xl">${props.title}</span>
<form class="[&_div]:mb-4 [&_input]:px-2 [&_textarea]:px-2" id="changeForm">
<input type="hidden" name="code" value="${props.code}">
<input type="hidden" name="code" id="codeField" value="${props.code}">
<div class="inline-flex w-1/2 [&_*]:mr-4 mb-6">
<span>Рейтинг</span>
<input class="border-2 border-secondary flex-1" type="number" name="rating" max="5" min="1"
value="${props.rating || '1'}">
value="${props.rating || '1'}" id="ratingField">
</div>
<div class="inline-flex w-1/2 [&_*]:mr-4 flex-col">
<span>Особенности филиала</span>
<textarea class="border-2 border-secondary flex-1" type="text" name="features">${props.features || ''}</textarea>
<textarea class="border-2 border-secondary flex-1" type="text" name="features" id="featuresField">${props.features || ''}</textarea>
</div>
<div class="inline-flex w-1/2 [&_*]:mb-2 flex-col">
<span>Контактное лицо</span>
<input type="hidden" name="profileId" value="${props.contact.id || '0'}">
<input class="border-2 border-secondary flex-1" type="text" name="profileFullName" placeholder="ФИО" value="${props.contact.fullName || ''}"/>
<input class="border-2 border-secondary flex-1" type="email" name="profileEmail" placeholder="Email" value="${props.contact.email || ''}"/>
<input class="border-2 border-secondary flex-1" type="tel" name="profilePhone"
placeholder="Номер телефона"
value="${props.contact.phone || ''}"
/>
<div class="inline-flex">
<input type="radio" name="userCreationField" value="0" id="userCreationFieldFalse" ${selectedValueUser === 0 && 'checked'}>
<label class="mr-4" for="userCreationFieldFalse">Выбрать пользователя из уже существующих</label>
<input type="radio" name="userCreationField" value="1" id="userCreationFieldTrue" ${selectedValueUser === 1 && 'checked'}>
<label for="userCreationFieldTrue">Создать нового пользователя</label>
</div
<div id="userBlock">
</div>
</div>
<div class="inline-flex w-1/2 [&_*]:mr-4 flex-col">
<span>Количество сотрудников</span>
<input class="border-2 border-secondary flex-1" type="number" name="personalCount" min="0" value="${props.personalCount}"/>
<input class="border-2 border-secondary flex-1" type="number" name="personalCount" min="0" value="${props.personalCount}" id="personalCountField"/>
</div>
<button class="block bg-primary font-bold text-white px-4 py-2" type="submit">Изменить</button>
</form>
@ -51,19 +121,66 @@ const Modal = (props: ModalPropsType) => `
</div>
`
export const renderModal = (props: ModalPropsType, reRenderHandler: () => void) => {
const modal = $(Modal(props))
modal.find('form').on('submit', (e) => {
e.preventDefault()
const formData = new FormData(e.target)
axios.post('http://localhost/api/v2/office/edit', formData).then(() => {
modal.remove()
reRenderHandler()
registerEvents(modal, reRenderHandler, props)
$('body').append(modal)
}
const addModulesToModal = (props: ModalPropsType) => {
$('#userBlock').append(selectedValueUser === 0 ? UserSelectingBlock(props): UserCreationBlock(props))
}
const registerEvents = (modal: JQuery<HTMLElement>, reRenderHandler: () => void, props: ModalPropsType) => {
modal.find('form').on('submit',
(e) => {
e.preventDefault()
const fields = {
code: $('#codeField'),
contact_person_id: $('#profileIdField'),
features: $('#featuresField'),
person_count: $('#personalCountField'),
rating: $('#ratingField'),
}
const query: UpdateOfficeRequest = {
code: +fields.code.val()!,
contact_person_id: null,
features: fields.features.val() ? String(fields.features.val()) : null,
person_count: fields.person_count.val() ? +fields.person_count.val()! : null,
rating: fields.rating.val() ? +fields.rating.val()! : null,
}
Backend.office.update_office(query).then(
() => {
modal.remove();
reRenderHandler();
}
)
})
})
modal.find('#removeModal').on('click', () => {
modal.remove()
reRenderHandler()
})
$('body').append(modal)
modal.find('[type="radio"]').on('change', (e) => {
selectedValueUser = +e.target.value
console.log(selectedValueUser)
const mdNew = $(Modal(props))
modal.replaceWith(mdNew)
registerEvents(mdNew, reRenderHandler, props)
})
modal.find('select').on('change', e => {
console.log(e.target.value)
Backend.profile.getById(e.target.value).then(
data => {
selectedUser.id = String(data.id)
selectedUser.email = data.email || ''
selectedUser.phone = data.phone || ''
selectedUser.full_name = data.full_name || ''
}
)
})
addModulesToModal(props);
}

0
src/state.ts Normal file
View File

View File

@ -1,37 +1,9 @@
import {components} from "./server";
import {ListOfficesResponse} from './backend/types.ts'
export type QueryBlockType = {
aex_only: string
mst_pr_aex: string
mst_pr_virt: string
max_ves: string
max_obyom: string
max_ves_gm: string
max_obyom_gm: string
max_l_gm: string
max_w_gm: string
max_h_gm: string
person_count: string
features: string
contact_profile: {
id: number
fullName: string
email: string,
phone: string
} | null
features_changeable: string
}
export interface BlockPropsType extends ServerResponseType {
}
export type ServerResponseType = {
[x in keyof components["schemas"]['JdeOfficeDetailResponse']]: components["schemas"]['JdeOfficeDetailResponse'][x]
}
export type BlocksDataValueType = {
block: JQuery<HTMLElement>,
data: ServerResponseType
data: ListOfficesResponse[0]
}
export type FilterType = Omit<{ [x in keyof ServerResponseType]?: ServerResponseType[x] | null } & {
export type FilterType = Omit<ListOfficesResponse[0] & {
fullTextSearch?: string | null,
}, 'coords' | 'changeable_info'>

View File

@ -170,6 +170,13 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/axios@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
integrity sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==
dependencies:
axios "*"
"@types/flexsearch@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@types/flexsearch/-/flexsearch-0.7.5.tgz#23827f2bacbc0cec386acc5f0383586a9202ad20"
@ -253,6 +260,15 @@ autoprefixer@^10.4.16:
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
axios@*:
version "1.6.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7"
integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.6.0:
version "1.6.0"
resolved "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz"