[FEAT]: add qr code #3
@ -8,3 +8,4 @@ VITE_API_PLACES = /place
|
||||
VITE_API_USERS = /profile
|
||||
VITE_API_USERS_ACCOUNT = /account
|
||||
VITE_API_USERS_PROFILE = /profile
|
||||
VITE_API_QR = /qr_code
|
10
.env.production
Normal file
10
.env.production
Normal 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
|
@ -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
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
BIN
public/assets/icons/qrcode.png
Normal file
BIN
public/assets/icons/qrcode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -3,7 +3,8 @@ import IGetPlacesRepo from '../../data/repository/IGetPlacesRepo';
|
||||
import { GetPlacesRO } from '../../data/response-object/protocols';
|
||||
import GettingPlacesUsecase from '../getPlaceUsecase';
|
||||
|
||||
const mockedRO: GetPlacesRO = {
|
||||
const mockedRO: GetPlacesRO = [
|
||||
{
|
||||
availableServices: [''],
|
||||
createdAt: 'createdAt',
|
||||
id: 'id',
|
||||
@ -12,7 +13,8 @@ const mockedRO: GetPlacesRO = {
|
||||
placeType: 'continent',
|
||||
updatedAt: 'updatedTime',
|
||||
qr: null,
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
const model = new PlacesModel(mockedRO);
|
||||
const mockedRepo: IGetPlacesRepo = jest.fn().mockImplementation(async () => model);
|
||||
|
42
src/driven/utils/components/modal/Modal.tsx
Normal file
42
src/driven/utils/components/modal/Modal.tsx
Normal 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,
|
||||
);
|
||||
}
|
@ -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}`,
|
||||
|
@ -5,4 +5,5 @@ export const icons = {
|
||||
logoBlack: `${baseIconsUrl}logo-black.svg`,
|
||||
users: `${baseIconsUrl}users.svg`,
|
||||
createUser: `${baseIconsUrl}createuser.svg`,
|
||||
qrcode: `${baseIconsUrl}qrcode.png`,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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<React.SetStateAction<string>>;
|
||||
|
@ -9,10 +9,11 @@ export default function TableRowView(props: ITableRowProps) {
|
||||
const columns = rowItemsTitle.map((rowItemTitle, index) => {
|
||||
return (
|
||||
<RowItem
|
||||
key={(rowItemTitle || 'row') + index}
|
||||
rowData={rowData}
|
||||
key={((rowItemTitle as string) || 'row') + index}
|
||||
hasCheckbox={index === 0}
|
||||
isSelected={isSelected}
|
||||
title={rowItemTitle}
|
||||
title={rowItemTitle as string}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -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<React.SetStateAction<string>>;
|
||||
|
@ -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 <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) {
|
||||
const { title, hasCheckbox, isSelected } = props;
|
||||
const { title, hasCheckbox, isSelected, rowData } = props;
|
||||
return (
|
||||
<td className={`px-1 py-2 ${isSelected ? 'bg-primary-100' : ''}`}>
|
||||
<div className='w-full flex'>
|
||||
@ -20,7 +63,7 @@ export default function RowItem(props: IRowItemProp) {
|
||||
<span className={`${isSelected ? 'visible' : 'hidden'} transition-all`}>✓</span>
|
||||
</span>
|
||||
)}
|
||||
{title}
|
||||
{typeof title === 'string' ? title : <QrCodeButton rowData={rowData} QrCodeData={title as QrPlace} />}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
@ -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<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 { placesData } = usePlacesListVM({
|
||||
useGetPlacesList,
|
||||
});
|
||||
|
||||
return <PlacesListView placesList={placesData} selectedRowId={selectedRowId} setSelectedRowId={setSelectedRowId} />;
|
||||
}
|
||||
|
@ -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) {
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
const tableTitles: Pick<Places, 'name' | 'placeType' | 'qr'> = {
|
||||
const tableTitles = {
|
||||
name: staticMessages.global.title,
|
||||
placeType: staticMessages.global.placeType,
|
||||
qr: staticMessages.global.qrCode,
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user