Merge pull request '[FEAT]: add qr code' (#3) from feature/qr into develop

Reviewed-on: http://10.244.188.80:30210/behnam/diapal-panel/pulls/3
This commit is contained in:
behnam 2023-05-23 12:39:42 +00:00
commit 840aa41fb6
22 changed files with 230 additions and 27 deletions

View File

@ -8,3 +8,4 @@ VITE_API_PLACES = /place
VITE_API_USERS = /profile VITE_API_USERS = /profile
VITE_API_USERS_ACCOUNT = /account VITE_API_USERS_ACCOUNT = /account
VITE_API_USERS_PROFILE = /profile VITE_API_USERS_PROFILE = /profile
VITE_API_QR = /qr_code

10
.env.production Normal file
View File

@ -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

View File

@ -1,8 +1,19 @@
version: "3.3" version: "3.3"
services: services:
dipal-admin-prod: dipal-admin:
ports: ports:
- 8000:80 - 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: build:
context: . context: .
dockerfile: dockerfile.prod dockerfile: dockerfile.prod

5
package-lock.json generated
View File

@ -7110,6 +7110,11 @@
"integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==",
"dev": true "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": { "querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View File

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"axios": "^1.3.4", "axios": "^1.3.4",
"qrcode.react": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,7 +1,14 @@
export type QrPlace = {
oneTime: false;
placeId: string;
userId: string;
id: string;
};
type Places = { type Places = {
placeType: string; placeType: string;
name: string; name: string;
qr: null | string; qr: null | QrPlace;
id: string; id: string;
parentId: string | null; parentId: string | null;
}; };

View File

@ -1,4 +1,4 @@
import Places from '../entity/placeEntity'; import Places, { QrPlace } from '../entity/placeEntity';
class PlacesModel { class PlacesModel {
private placesList: Places[]; private placesList: Places[];
@ -16,6 +16,23 @@ class PlacesModel {
getTitle(): string { getTitle(): string {
return this.modelTitle; 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; export default PlacesModel;

View File

@ -3,7 +3,8 @@ import IGetPlacesRepo from '../../data/repository/IGetPlacesRepo';
import { GetPlacesRO } from '../../data/response-object/protocols'; import { GetPlacesRO } from '../../data/response-object/protocols';
import GettingPlacesUsecase from '../getPlaceUsecase'; import GettingPlacesUsecase from '../getPlaceUsecase';
const mockedRO: GetPlacesRO = { const mockedRO: GetPlacesRO = [
{
availableServices: [''], availableServices: [''],
createdAt: 'createdAt', createdAt: 'createdAt',
id: 'id', id: 'id',
@ -12,7 +13,8 @@ const mockedRO: GetPlacesRO = {
placeType: 'continent', placeType: 'continent',
updatedAt: 'updatedTime', updatedAt: 'updatedTime',
qr: null, qr: null,
}; },
];
const model = new PlacesModel(mockedRO); const model = new PlacesModel(mockedRO);
const mockedRepo: IGetPlacesRepo = jest.fn().mockImplementation(async () => model); const mockedRepo: IGetPlacesRepo = jest.fn().mockImplementation(async () => model);

View File

@ -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<HTMLDivElement>(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(
<div
onClick={closeModal}
ref={modalRef}
className='fixed top-0 left-0 z-20 w-screen h-screen p-2 rounded-md text-black flex justify-center items-center'
>
<div
className='max-w-[60%] max-h-[90%] p-4 rounded-sm shadow-lg bg-white overflow-auto'
onClick={(e) => {
e.stopPropagation();
}}
>
{children}
</div>
</div>,
el.current,
);
}

View File

@ -30,6 +30,7 @@ export const apiUrls = {
core: { core: {
getPlaces: `${baseApiUrl}${ENVs.apiGetPlaces}`, getPlaces: `${baseApiUrl}${ENVs.apiGetPlaces}`,
getUsers: `${baseApiUrl}${ENVs.apiGetUsers}`, getUsers: `${baseApiUrl}${ENVs.apiGetUsers}`,
getQrs: `${baseApiUrl}${ENVs.apiQr}`,
createUserAccount: `${baseApiUrl}${ENVs.apiCreateUserAccount}`, createUserAccount: `${baseApiUrl}${ENVs.apiCreateUserAccount}`,
createUserProfile: `${baseApiUrl}${ENVs.apiCreateUserProfile}`, createUserProfile: `${baseApiUrl}${ENVs.apiCreateUserProfile}`,
createMember: `${baseApiUrl}${ENVs.apiCreateMember}`, createMember: `${baseApiUrl}${ENVs.apiCreateMember}`,

View File

@ -5,4 +5,5 @@ export const icons = {
logoBlack: `${baseIconsUrl}logo-black.svg`, logoBlack: `${baseIconsUrl}logo-black.svg`,
users: `${baseIconsUrl}users.svg`, users: `${baseIconsUrl}users.svg`,
createUser: `${baseIconsUrl}createuser.svg`, createUser: `${baseIconsUrl}createuser.svg`,
qrcode: `${baseIconsUrl}qrcode.png`,
}; };

View File

@ -7,6 +7,7 @@ export const ENVs = {
apiGetPlaces: process.env.VITE_API_PLACES, apiGetPlaces: process.env.VITE_API_PLACES,
apiGetUsers: process.env.VITE_API_USERS, apiGetUsers: process.env.VITE_API_USERS,
apiCreateUserAccount: process.env.VITE_API_USERS_ACCOUNT, apiCreateUserAccount: process.env.VITE_API_USERS_ACCOUNT,
apiQr: process.env.VITE_API_QR,
apiCreateUserProfile: process.env.VITE_API_USERS_PROFILE, apiCreateUserProfile: process.env.VITE_API_USERS_PROFILE,
apiCreateMember: process.env.VITE_API_CREATE_MEMBER, apiCreateMember: process.env.VITE_API_CREATE_MEMBER,
}; };

View File

@ -24,6 +24,10 @@ export const staticMessages = {
createUser: 'user created successfully', createUser: 'user created successfully',
createMember: 'member created successfully', createMember: 'member created successfully',
}, },
and: 'and',
canUseFor: 'can use for',
oneTime: 'one time',
multipleTimes: 'multiple times',
}, },
service: { service: {
errors: { errors: {

View File

@ -1,7 +1,9 @@
import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity';
export interface ITableRowInfra { export interface ITableRowInfra {
selectedRowId: string; selectedRowId: string;
rowData: { rowData: {
rowItemsTitle: (string | null)[]; rowItemsTitle: (string | null | QrPlace)[];
rowId: string; rowId: string;
}; };
setSelectedRowId: React.Dispatch<React.SetStateAction<string>>; setSelectedRowId: React.Dispatch<React.SetStateAction<string>>;

View File

@ -9,10 +9,11 @@ export default function TableRowView(props: ITableRowProps) {
const columns = rowItemsTitle.map((rowItemTitle, index) => { const columns = rowItemsTitle.map((rowItemTitle, index) => {
return ( return (
<RowItem <RowItem
key={(rowItemTitle || 'row') + index} rowData={rowData}
key={((rowItemTitle as string) || 'row') + index}
hasCheckbox={index === 0} hasCheckbox={index === 0}
isSelected={isSelected} isSelected={isSelected}
title={rowItemTitle} title={rowItemTitle as string}
/> />
); );
}); });

View File

@ -1,7 +1,9 @@
import { QrPlace } from '~/business-logic/core/places/common/entity/placeEntity';
export interface ITableRowProps { export interface ITableRowProps {
isSelected: boolean; isSelected: boolean;
rowData: { rowData: {
rowItemsTitle: (string | null)[]; rowItemsTitle: (string | null | QrPlace)[];
rowId: string; rowId: string;
}; };
setSelectedRowId: React.Dispatch<React.SetStateAction<string>>; setSelectedRowId: React.Dispatch<React.SetStateAction<string>>;

View File

@ -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 { interface IRowItemProp {
title: string | null; title: string | null | QrPlace;
hasCheckbox: boolean; hasCheckbox: boolean;
isSelected: 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 <QRCodeCanvas id='qrCode' value={message} size={150} bgColor='#fff' level='H' />;
}
function QrCodeButton(props: { QrCodeData: QrPlace; rowData: ITableRowInfra['rowData'] }) {
const { QrCodeData, rowData } = props;
const [isShowModal, setIsShowModal] = useState(false);
if (!QrCodeData) return null;
return (
<>
{isShowModal && (
<Modal onCloseCallback={() => setIsShowModal(false)}>
<QrCodeReader rowData={rowData} QrCodeData={QrCodeData} />
</Modal>
)}
<button
onClick={(e) => {
e.stopPropagation();
setIsShowModal(true);
}}
className='w-full h-full mx-auto flex justify-center items-center'
>
<img className='w-6 h-6' src={icons.qrcode} alt='qrcode' />
</button>
</>
);
} }
export default function RowItem(props: IRowItemProp) { export default function RowItem(props: IRowItemProp) {
const { title, hasCheckbox, isSelected } = props; const { title, hasCheckbox, isSelected, rowData } = props;
return ( return (
<td className={`px-1 py-2 ${isSelected ? 'bg-primary-100' : ''}`}> <td className={`px-1 py-2 ${isSelected ? 'bg-primary-100' : ''}`}>
<div className='w-full flex'> <div className='w-full flex'>
@ -20,7 +63,7 @@ export default function RowItem(props: IRowItemProp) {
<span className={`${isSelected ? 'visible' : 'hidden'} transition-all`}>&#10003;</span> <span className={`${isSelected ? 'visible' : 'hidden'} transition-all`}>&#10003;</span>
</span> </span>
)} )}
{title} {typeof title === 'string' ? title : <QrCodeButton rowData={rowData} QrCodeData={title as QrPlace} />}
</div> </div>
</td> </td>
); );

View File

@ -1,3 +1,5 @@
/* eslint-disable consistent-return */
/* eslint-disable no-underscore-dangle */
import React from 'react'; import React from 'react';
import getPlaces from '~/business-logic/core/places/get-places'; import getPlaces from '~/business-logic/core/places/get-places';
import getPlacesAdapter from '~/driven/adapters/get-places-adapter/getPlacesAdapter'; 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 { prepareStateManagementForVM } from '~/driven/utils/helpers/globalHelpers';
import useGetNavigatorAndTokenUpdater from '~/driven/utils/helpers/hooks/getNavigatorAndAccessTokenUpdator'; import useGetNavigatorAndTokenUpdater from '~/driven/utils/helpers/hooks/getNavigatorAndAccessTokenUpdator';
import AdminUserModel from '~/business-logic/generic/admin-user/common/data/model/adminUserModel'; 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 PlacesListView from '../view/PlacesListView';
import usePlacesListVM from '../viewmodel/placesListVM'; import usePlacesListVM from '../viewmodel/placesListVM';
import placesListModel from '../model/placesListModel'; 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 prepareTheLogicForModel = () => {
const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater(); const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater();
@ -26,12 +47,43 @@ export interface IPlacesListProps {
export default function PlacessList(props: IPlacesListProps) { export default function PlacessList(props: IPlacesListProps) {
const { selectedRowId, setSelectedRowId } = props; const { selectedRowId, setSelectedRowId } = props;
const { getingPlacesLogic, url } = prepareTheLogicForModel(); const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater();
const placesModel = async () => await placesListModel(getingPlacesLogic);
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<QrCodeResponse>(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<PlacesModel>(url, placesModel); const useGetPlacesList = prepareStateManagementForVM<PlacesModel>(url, placesModel);
const { placesData } = usePlacesListVM({ const { placesData } = usePlacesListVM({
useGetPlacesList, useGetPlacesList,
}); });
return <PlacesListView placesList={placesData} selectedRowId={selectedRowId} setSelectedRowId={setSelectedRowId} />; return <PlacesListView placesList={placesData} selectedRowId={selectedRowId} setSelectedRowId={setSelectedRowId} />;
} }

View File

@ -1,7 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { staticMessages } from '~/driven/utils/constants/staticMessages'; import { staticMessages } from '~/driven/utils/constants/staticMessages';
import Loading from '~/driven/utils/components/loading/Loading'; 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 TableRow from '../../common/table-row';
import { IPlacesListProps } from './protocols'; import { IPlacesListProps } from './protocols';
@ -33,7 +32,7 @@ export default function UsersListView(props: IPlacesListProps) {
<Loading /> <Loading />
</div> </div>
); );
const tableTitles: Pick<Places, 'name' | 'placeType' | 'qr'> = { const tableTitles = {
name: staticMessages.global.title, name: staticMessages.global.title,
placeType: staticMessages.global.placeType, placeType: staticMessages.global.placeType,
qr: staticMessages.global.qrCode, qr: staticMessages.global.qrCode,

View File

@ -17,6 +17,7 @@ export default function index() {
const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater(); const { accessTokenUpdateHandler, notLoginAuth, userData } = useGetNavigatorAndTokenUpdater();
const [error, setError] = useState<{ message: string; type: 'error' | 'success' }>({ message: '', type: 'error' }); const [error, setError] = useState<{ message: string; type: 'error' | 'success' }>({ message: '', type: 'error' });
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true); const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const onSubmitMember = async (e: React.FormEvent) => { const onSubmitMember = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {