diff --git a/.env.development b/.env.development index 233e901..50eda8e 100644 --- a/.env.development +++ b/.env.development @@ -7,4 +7,5 @@ VITE_API_CREATE_MEMBER = /user_place/members VITE_API_PLACES = /place VITE_API_USERS = /profile VITE_API_USERS_ACCOUNT = /account -VITE_API_USERS_PROFILE = /profile \ No newline at end of file +VITE_API_USERS_PROFILE = /profile +VITE_API_QR = /qr_code \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..233e901 --- /dev/null +++ b/.env.production @@ -0,0 +1,10 @@ +VITE_API_ORIGIN = https://admin.dev.dipal.ru/api/v1 +VITE_API_AUTH_ORIGIN = https://auth.dev.dipal.ru/api/v1/auth +VITE_API_AUTH_PHONENUMBER = /start-challenge +VITE_API_AUTH_LOGIN = /login +VITE_API_AUTH_REFRESH = /refresh-token +VITE_API_CREATE_MEMBER = /user_place/members +VITE_API_PLACES = /place +VITE_API_USERS = /profile +VITE_API_USERS_ACCOUNT = /account +VITE_API_USERS_PROFILE = /profile \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 55ebaaa..a430b0b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,8 +1,19 @@ version: "3.3" services: - dipal-admin-prod: + dipal-admin: ports: - 8000:80 + environment: + - VITE_API_ORIGIN=https://admin.dev.dipal.ru/api/v1 + - VITE_API_AUTH_ORIGIN=https://auth.dev.dipal.ru/api/v1/auth + - VITE_API_AUTH_PHONENUMBER=/start-challenge + - VITE_API_AUTH_LOGIN=/login + - VITE_API_AUTH_REFRESH=/refresh-token + - VITE_API_CREATE_MEMBER=/user_place/members + - VITE_API_PLACES=/place + - VITE_API_USERS=/profile + - VITE_API_USERS_ACCOUNT=/account + - VITE_API_USERS_PROFILE=/profile build: context: . dockerfile: dockerfile.prod \ No newline at end of file diff --git a/dockerfile.prod b/dockerfile.prod index 493cdd5..a2122da 100644 --- a/dockerfile.prod +++ b/dockerfile.prod @@ -18,4 +18,4 @@ USER nginx EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 78b9b10..4ca65cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7110,6 +7110,11 @@ "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "dev": true }, + "qrcode.react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==" + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", diff --git a/package.json b/package.json index 223f7a6..fea4397 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@reduxjs/toolkit": "^1.9.3", "axios": "^1.3.4", + "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.5", diff --git a/public/assets/icons/qrcode.png b/public/assets/icons/qrcode.png new file mode 100644 index 0000000..9e40ac1 Binary files /dev/null and b/public/assets/icons/qrcode.png differ diff --git a/src/business-logic/core/places/common/entity/placeEntity.ts b/src/business-logic/core/places/common/entity/placeEntity.ts index b171c44..d6cb290 100644 --- a/src/business-logic/core/places/common/entity/placeEntity.ts +++ b/src/business-logic/core/places/common/entity/placeEntity.ts @@ -1,7 +1,14 @@ +export type QrPlace = { + oneTime: false; + placeId: string; + userId: string; + id: string; +}; + type Places = { placeType: string; name: string; - qr: null | string; + qr: null | QrPlace; id: string; parentId: string | null; }; diff --git a/src/business-logic/core/places/common/model/placesModel.ts b/src/business-logic/core/places/common/model/placesModel.ts index 9b73779..bb96839 100644 --- a/src/business-logic/core/places/common/model/placesModel.ts +++ b/src/business-logic/core/places/common/model/placesModel.ts @@ -1,4 +1,4 @@ -import Places from '../entity/placeEntity'; +import Places, { QrPlace } from '../entity/placeEntity'; class PlacesModel { private placesList: Places[]; @@ -16,6 +16,23 @@ class PlacesModel { getTitle(): string { return this.modelTitle; } + + private updatePlaces(updatedPlaces: Places[]) { + this.placesList = updatedPlaces; + } + + setQrFor(qrCodesToAdd: QrPlace[]): Places[] { + const updatedPlaces = this.placesList.map((place) => { + const relatedQrCode = qrCodesToAdd.find((qrItem) => qrItem.placeId === place.id); + + return { + ...place, + qr: relatedQrCode || null, + }; + }); + this.updatePlaces(updatedPlaces); + return updatedPlaces; + } } export default PlacesModel; diff --git a/src/business-logic/core/places/get-places/usecase/__test__/getPlacesUsecase.test.ts b/src/business-logic/core/places/get-places/usecase/__test__/getPlacesUsecase.test.ts index 947836c..cc702b1 100644 --- a/src/business-logic/core/places/get-places/usecase/__test__/getPlacesUsecase.test.ts +++ b/src/business-logic/core/places/get-places/usecase/__test__/getPlacesUsecase.test.ts @@ -3,16 +3,18 @@ import IGetPlacesRepo from '../../data/repository/IGetPlacesRepo'; import { GetPlacesRO } from '../../data/response-object/protocols'; import GettingPlacesUsecase from '../getPlaceUsecase'; -const mockedRO: GetPlacesRO = { - availableServices: [''], - createdAt: 'createdAt', - id: 'id', - name: 'name', - parentId: null, - placeType: 'continent', - updatedAt: 'updatedTime', - qr: null, -}; +const mockedRO: GetPlacesRO = [ + { + availableServices: [''], + createdAt: 'createdAt', + id: 'id', + name: 'name', + parentId: null, + placeType: 'continent', + updatedAt: 'updatedTime', + qr: null, + }, +]; const model = new PlacesModel(mockedRO); const mockedRepo: IGetPlacesRepo = jest.fn().mockImplementation(async () => model); diff --git a/src/driven/utils/components/modal/Modal.tsx b/src/driven/utils/components/modal/Modal.tsx new file mode 100644 index 0000000..11740d9 --- /dev/null +++ b/src/driven/utils/components/modal/Modal.tsx @@ -0,0 +1,42 @@ +/* eslint-disable no-unused-expressions */ +import React from 'react'; +import ReactDOM from 'react-dom'; + +export interface ModalInterface { + onCloseCallback?: () => unknown; +} +export default function Modal({ onCloseCallback, children }: ModalInterface & React.PropsWithChildren) { + const modalRef = React.useRef(null); + const el = React.useRef(document.createElement('div')); + + const closeModal = () => { + el.current.remove(); + if (typeof onCloseCallback !== 'undefined') onCloseCallback(); + }; + React.useEffect(() => { + const portal = document.getElementById('root'); + portal?.appendChild(el.current); + + return () => { + if (typeof onCloseCallback !== 'undefined') onCloseCallback(); + return el.current?.remove(); + }; + }, []); + return ReactDOM.createPortal( +
+
{ + e.stopPropagation(); + }} + > + {children} +
+
, + el.current, + ); +} diff --git a/src/driven/utils/configs/appConfig.ts b/src/driven/utils/configs/appConfig.ts index 93bd570..b4742ae 100644 --- a/src/driven/utils/configs/appConfig.ts +++ b/src/driven/utils/configs/appConfig.ts @@ -30,6 +30,7 @@ export const apiUrls = { core: { getPlaces: `${baseApiUrl}${ENVs.apiGetPlaces}`, getUsers: `${baseApiUrl}${ENVs.apiGetUsers}`, + getQrs: `${baseApiUrl}${ENVs.apiQr}`, createUserAccount: `${baseApiUrl}${ENVs.apiCreateUserAccount}`, createUserProfile: `${baseApiUrl}${ENVs.apiCreateUserProfile}`, createMember: `${baseApiUrl}${ENVs.apiCreateMember}`, diff --git a/src/driven/utils/constants/assertUrls.ts b/src/driven/utils/constants/assertUrls.ts index 66c35ac..938b105 100644 --- a/src/driven/utils/constants/assertUrls.ts +++ b/src/driven/utils/constants/assertUrls.ts @@ -5,4 +5,5 @@ export const icons = { logoBlack: `${baseIconsUrl}logo-black.svg`, users: `${baseIconsUrl}users.svg`, createUser: `${baseIconsUrl}createuser.svg`, + qrcode: `${baseIconsUrl}qrcode.png`, }; diff --git a/src/driven/utils/constants/envs.ts b/src/driven/utils/constants/envs.ts index 102978c..428a48c 100644 --- a/src/driven/utils/constants/envs.ts +++ b/src/driven/utils/constants/envs.ts @@ -7,6 +7,7 @@ export const ENVs = { apiGetPlaces: process.env.VITE_API_PLACES, apiGetUsers: process.env.VITE_API_USERS, apiCreateUserAccount: process.env.VITE_API_USERS_ACCOUNT, + apiQr: process.env.VITE_API_QR, apiCreateUserProfile: process.env.VITE_API_USERS_PROFILE, apiCreateMember: process.env.VITE_API_CREATE_MEMBER, }; diff --git a/src/driven/utils/constants/staticMessages.ts b/src/driven/utils/constants/staticMessages.ts index 3acc2a0..a666000 100644 --- a/src/driven/utils/constants/staticMessages.ts +++ b/src/driven/utils/constants/staticMessages.ts @@ -24,6 +24,10 @@ export const staticMessages = { createUser: 'user created successfully', createMember: 'member created successfully', }, + and: 'and', + canUseFor: 'can use for', + oneTime: 'one time', + multipleTimes: 'multiple times', }, service: { errors: { diff --git a/src/driving/application/core/common/table-row/infra/protocols.ts b/src/driving/application/core/common/table-row/infra/protocols.ts index c2a94de..df9424d 100644 --- a/src/driving/application/core/common/table-row/infra/protocols.ts +++ b/src/driving/application/core/common/table-row/infra/protocols.ts @@ -1,7 +1,9 @@ +import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity'; + export interface ITableRowInfra { selectedRowId: string; rowData: { - rowItemsTitle: (string | null)[]; + rowItemsTitle: (string | null | QrPlace)[]; rowId: string; }; setSelectedRowId: React.Dispatch>; diff --git a/src/driving/application/core/common/table-row/view/TableRow.tsx b/src/driving/application/core/common/table-row/view/TableRow.tsx index 5156de4..7185fbc 100644 --- a/src/driving/application/core/common/table-row/view/TableRow.tsx +++ b/src/driving/application/core/common/table-row/view/TableRow.tsx @@ -9,10 +9,11 @@ export default function TableRowView(props: ITableRowProps) { const columns = rowItemsTitle.map((rowItemTitle, index) => { return ( ); }); diff --git a/src/driving/application/core/common/table-row/view/protocols.ts b/src/driving/application/core/common/table-row/view/protocols.ts index eeba1fb..e9c9058 100644 --- a/src/driving/application/core/common/table-row/view/protocols.ts +++ b/src/driving/application/core/common/table-row/view/protocols.ts @@ -1,7 +1,9 @@ +import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity'; + export interface ITableRowProps { isSelected: boolean; rowData: { - rowItemsTitle: (string | null)[]; + rowItemsTitle: (string | null | QrPlace)[]; rowId: string; }; setSelectedRowId: React.Dispatch>; diff --git a/src/driving/application/core/common/table-row/view/table-row-item/view/RowItem.tsx b/src/driving/application/core/common/table-row/view/table-row-item/view/RowItem.tsx index e15e71b..d01181f 100644 --- a/src/driving/application/core/common/table-row/view/table-row-item/view/RowItem.tsx +++ b/src/driving/application/core/common/table-row/view/table-row-item/view/RowItem.tsx @@ -1,13 +1,56 @@ -import React from 'react'; +/* eslint-disable import/no-extraneous-dependencies */ +import React, { useState } from 'react'; +import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity'; +import Modal from '~/driven/utils/components/modal/Modal'; +import { icons } from '~/driven/utils/constants/assertUrls'; +import { QRCodeCanvas } from 'qrcode.react'; +import { staticMessages } from '~/driven/utils/constants/staticMessages'; +import { ITableRowInfra } from '../../../infra/protocols'; interface IRowItemProp { - title: string | null; + title: string | null | QrPlace; hasCheckbox: boolean; isSelected: boolean; + rowData: ITableRowInfra['rowData']; +} + +function QrCodeReader(props: { QrCodeData: QrPlace; rowData: ITableRowInfra['rowData'] }) { + const { QrCodeData, rowData } = props; + const placeType = rowData.rowItemsTitle[1]; + const placeTitle = rowData.rowItemsTitle[0]; + const isOneTime = !QrCodeData.oneTime ? staticMessages.global.multipleTimes : staticMessages.global.oneTime; + + const message = `${placeType} ${placeTitle} ${staticMessages.global.and} ${staticMessages.global.canUseFor} ${isOneTime}`; + return ; +} + +function QrCodeButton(props: { QrCodeData: QrPlace; rowData: ITableRowInfra['rowData'] }) { + const { QrCodeData, rowData } = props; + const [isShowModal, setIsShowModal] = useState(false); + + if (!QrCodeData) return null; + return ( + <> + {isShowModal && ( + setIsShowModal(false)}> + + + )} + + + ); } export default function RowItem(props: IRowItemProp) { - const { title, hasCheckbox, isSelected } = props; + const { title, hasCheckbox, isSelected, rowData } = props; return (
@@ -20,7 +63,7 @@ export default function RowItem(props: IRowItemProp) { )} - {title} + {typeof title === 'string' ? title : }
); diff --git a/src/driving/application/core/places-list/infra/PlacesList.tsx b/src/driving/application/core/places-list/infra/PlacesList.tsx index b3d0422..571b93f 100644 --- a/src/driving/application/core/places-list/infra/PlacesList.tsx +++ b/src/driving/application/core/places-list/infra/PlacesList.tsx @@ -1,3 +1,5 @@ +/* eslint-disable consistent-return */ +/* eslint-disable no-underscore-dangle */ import React from 'react'; import getPlaces from '~/business-logic/core/places/get-places'; import getPlacesAdapter from '~/driven/adapters/get-places-adapter/getPlacesAdapter'; @@ -5,10 +7,29 @@ import PlacesModel from '~/business-logic/core/places/common/model/placesModel'; import { prepareStateManagementForVM } from '~/driven/utils/helpers/globalHelpers'; import useGetNavigatorAndTokenUpdater from '~/driven/utils/helpers/hooks/getNavigatorAndAccessTokenUpdator'; import AdminUserModel from '~/business-logic/generic/admin-user/common/data/model/adminUserModel'; +import { apiUrls } from '~/driven/utils/configs/appConfig'; +import { HttpOptionsType } from '~/driven/boundaries/http-boundary/protocols'; +import { HTTPPovider } from '~/driven/boundaries/http-boundary/httpBoundary'; +import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity'; import PlacesListView from '../view/PlacesListView'; import usePlacesListVM from '../viewmodel/placesListVM'; import placesListModel from '../model/placesListModel'; +type QrCodeResponse = { + one_time: false; + place_id: string; + user_id: string; + _id: string; +}[]; +const QrCodeRO = (response: QrCodeResponse): QrPlace[] => { + return response.map((qrCode) => ({ + id: qrCode._id, + oneTime: qrCode.one_time, + placeId: qrCode.place_id, + userId: qrCode.user_id, + })); +}; + const prepareTheLogicForModel = () => { const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater(); @@ -26,12 +47,43 @@ export interface IPlacesListProps { export default function PlacessList(props: IPlacesListProps) { const { selectedRowId, setSelectedRowId } = props; - const { getingPlacesLogic, url } = prepareTheLogicForModel(); - const placesModel = async () => await placesListModel(getingPlacesLogic); + const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater(); + const getQrCodes = async () => { + try { + // url + const apiUrl = apiUrls.core.getQrs; + // options + const apiOptions: HttpOptionsType = { + url: apiUrl, + }; + // request + const userToken = { + accessToken: userData.user?.adminUserData.accessToken || null, + refreshToken: userData.user?.adminUserData.refreshToken || null, + }; + const httpProvider = new HTTPPovider(userToken, accessTokenUpdateHandler, notLoginAuth); + const response = await httpProvider.request(apiOptions); + const qrCodeList = QrCodeRO(response); + return qrCodeList; + // update the query + } catch (error) { + console.log(error); + } + }; + + const { getingPlacesLogic, url } = prepareTheLogicForModel(); + const getPlacesWithQrLogic = async () => { + const placesList = await getingPlacesLogic(); + const qrCodesList = await getQrCodes(); + placesList.setQrFor(qrCodesList || []); + return placesList; + }; + const placesModel = async () => await placesListModel(getPlacesWithQrLogic); const useGetPlacesList = prepareStateManagementForVM(url, placesModel); const { placesData } = usePlacesListVM({ useGetPlacesList, }); + return ; } diff --git a/src/driving/application/core/places-list/view/PlacesListView.tsx b/src/driving/application/core/places-list/view/PlacesListView.tsx index 8258417..8409eb1 100644 --- a/src/driving/application/core/places-list/view/PlacesListView.tsx +++ b/src/driving/application/core/places-list/view/PlacesListView.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { staticMessages } from '~/driven/utils/constants/staticMessages'; import Loading from '~/driven/utils/components/loading/Loading'; -import Places from '~/business-logic/core/places/common/entity/placeEntity'; import TableRow from '../../common/table-row'; import { IPlacesListProps } from './protocols'; @@ -33,7 +32,7 @@ export default function UsersListView(props: IPlacesListProps) { ); - const tableTitles: Pick = { + const tableTitles = { name: staticMessages.global.title, placeType: staticMessages.global.placeType, qr: staticMessages.global.qrCode, diff --git a/src/driving/main/pages/index.tsx b/src/driving/main/pages/index.tsx index 8573c65..f2edfca 100644 --- a/src/driving/main/pages/index.tsx +++ b/src/driving/main/pages/index.tsx @@ -17,6 +17,7 @@ export default function index() { const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater(); const [error, setError] = useState<{ message: string; type: 'error' | 'success' }>({ message: '', type: 'error' }); const [isSubmitDisabled, setIsSubmitDisabled] = useState(true); + const onSubmitMember = async (e: React.FormEvent) => { e.preventDefault(); try {