diff --git a/package-lock.json b/package-lock.json
index 7ed9811..5bd8ffc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@fontsource/open-sans": "^5.1.0",
"@lottiefiles/react-lottie-player": "^3.5.4",
"@pbe/react-yandex-maps": "^1.2.5",
+ "@reduxjs/toolkit": "^2.5.0",
"@types/react": "^18.3.12",
"dayjs": "^1.11.13",
"express": "^4.21.1",
@@ -29,6 +30,7 @@
"react-i18next": "^15.1.1",
"react-icons": "^5.3.0",
"react-phone-number-input": "^3.4.9",
+ "react-redux": "^9.2.0",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
@@ -2639,6 +2641,38 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
+ "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -2802,6 +2836,11 @@
"@types/react": "*"
}
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@types/yandex-maps": {
"version": "2.1.29",
"resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz",
@@ -8735,6 +8774,28 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
@@ -8892,6 +8953,19 @@
"recursive-watch": "bin.js"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -9009,6 +9083,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -10249,6 +10328,14 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index cde405f..c4e7e31 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@fontsource/open-sans": "^5.1.0",
"@lottiefiles/react-lottie-player": "^3.5.4",
"@pbe/react-yandex-maps": "^1.2.5",
+ "@reduxjs/toolkit": "^2.5.0",
"@types/react": "^18.3.12",
"dayjs": "^1.11.13",
"express": "^4.21.1",
@@ -37,6 +38,7 @@
"react-i18next": "^15.1.1",
"react-icons": "^5.3.0",
"react-phone-number-input": "^3.4.9",
+ "react-redux": "^9.2.0",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
diff --git a/src/__data__/service/api.ts b/src/__data__/service/api.ts
new file mode 100644
index 0000000..752f43d
--- /dev/null
+++ b/src/__data__/service/api.ts
@@ -0,0 +1,41 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { getConfigValue } from '@brojs/cli';
+
+import { Master } from '../../models/api/master';
+
+type SuccessResponse
= {
+ success: true;
+ body: Body;
+};
+
+type ErrorResponse = {
+ success: false;
+ message: string;
+};
+
+type BaseResponse = SuccessResponse | ErrorResponse;
+
+export const api = createApi({
+ reducerPath: 'api',
+ baseQuery: fetchBaseQuery({ baseUrl: getConfigValue('dry-wash.api') }),
+ tagTypes: ['Masters'],
+ endpoints: (builder) => ({
+ getMasters: builder.query({
+ query: () => ({ url: '/arm/masters' }),
+ transformResponse: (response: BaseResponse) => {
+ if (response.success) {
+ return response.body;
+ }
+ },
+ providesTags: ['Masters'],
+ }),
+ addMaster: builder.mutation>({
+ query: (master) => ({
+ url: '/arm/masters',
+ method: 'POST',
+ body: master,
+ }),
+ invalidatesTags: ['Masters'],
+ }),
+ }),
+});
diff --git a/src/__data__/store.ts b/src/__data__/store.ts
new file mode 100644
index 0000000..dabb855
--- /dev/null
+++ b/src/__data__/store.ts
@@ -0,0 +1,14 @@
+import { configureStore } from '@reduxjs/toolkit';
+
+import { api } from './service/api';
+
+export const store = configureStore({
+ reducer: {
+ [api.reducerPath]: api.reducer,
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(api.middleware),
+});
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/app.tsx b/src/app.tsx
index 5bfd51d..31590e0 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,19 +1,23 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
+import { Provider } from 'react-redux';
import Routers from './routes';
import ErrorBoundary from './components/ErrorBoundary';
+import { store } from './__data__/store';
const App = () => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/MasterDrawer/MasterDrawer.tsx b/src/components/MasterDrawer/MasterDrawer.tsx
index 1f4ac79..fe790bb 100644
--- a/src/components/MasterDrawer/MasterDrawer.tsx
+++ b/src/components/MasterDrawer/MasterDrawer.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import {
Button,
FormControl,
@@ -18,21 +18,29 @@ import {
import { useTranslation } from 'react-i18next';
import { PhoneIcon } from '@chakra-ui/icons';
-import { armService } from '../../api/arm';
+import { api } from '../../__data__/service/api';
const MasterDrawer = ({ isOpen, onClose }) => {
- const { addMaster } = armService();
+ const [addMaster, { error, isSuccess }] = api.useAddMasterMutation();
const toast = useToast();
const [newMaster, setNewMaster] = useState({ name: '', phone: '' });
const handleSave = async () => {
- if (newMaster.name.trim() === '' || newMaster.phone.trim() === '') {
+ const trimMaster = {
+ phone: newMaster.phone.trim(),
+ name: newMaster.name.trim(),
+ };
+
+ if (trimMaster.name === '' || trimMaster.phone === '') {
return;
}
- try {
- await addMaster(newMaster);
+ addMaster(trimMaster);
+ };
+
+ useEffect(() => {
+ if (isSuccess) {
toast({
title: 'Мастер создан.',
description: `Мастер "${newMaster.name}" успешно добавлен.`,
@@ -42,7 +50,11 @@ const MasterDrawer = ({ isOpen, onClose }) => {
position: 'top-right',
});
onClose();
- } catch (error) {
+ }
+ }, [isSuccess]);
+
+ useEffect(() => {
+ if (error) {
toast({
title: 'Ошибка при создании мастера.',
description: 'Не удалось добавить мастера. Попробуйте еще раз.',
@@ -53,7 +65,7 @@ const MasterDrawer = ({ isOpen, onClose }) => {
});
console.error(error);
}
- };
+ }, [error]);
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.drawer',
diff --git a/src/components/MasterItem/MasterItem.tsx b/src/components/MasterItem/MasterItem.tsx
index 6ea75f4..8df73de 100644
--- a/src/components/MasterItem/MasterItem.tsx
+++ b/src/components/MasterItem/MasterItem.tsx
@@ -7,19 +7,6 @@ import { getTimeSlot } from '../../lib';
import EditableWrapper from '../Editable/Editable';
import { armService } from '../../api/arm';
-export interface Schedule {
- id: string;
- startWashTime: string;
- endWashTime: string;
-}
-
-export type MasterProps = {
- id: string;
- name: string;
- phone: string;
- schedule: Schedule[];
-};
-
const MasterItem = ({ name, phone, id, schedule }) => {
const { updateMaster } = armService();
const { t } = useTranslation('~', {
diff --git a/src/components/Masters/Masters.tsx b/src/components/Masters/Masters.tsx
index 7846fc8..a7aff65 100644
--- a/src/components/Masters/Masters.tsx
+++ b/src/components/Masters/Masters.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect } from 'react';
import {
Box,
Heading,
@@ -19,8 +19,7 @@ import { useTranslation } from 'react-i18next';
import MasterItem from '../MasterItem';
import MasterDrawer from '../MasterDrawer';
-import { armService } from '../../api/arm';
-import { MasterProps } from '../MasterItem/MasterItem';
+import { api } from '../../__data__/service/api';
const TABLE_HEADERS = [
'name' as const,
@@ -36,35 +35,23 @@ const Masters = () => {
keyPrefix: 'dry-wash.arm.master',
});
- const [masters, setMasters] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const { fetchMasters } = armService();
+ const {
+ data: masters,
+ error,
+ isLoading,
+ isSuccess,
+ } = api.useGetMastersQuery();
useEffect(() => {
- const loadMasters = async () => {
- setLoading(true);
-
- try {
- const data = await fetchMasters();
- setMasters(data.body);
- } catch (err) {
- setError(err.message);
- toast({
- title: t('error.title'),
- status: 'error',
- duration: 5000,
- isClosable: true,
- position: 'bottom-right',
- });
- } finally {
- setLoading(false);
- }
- };
-
- loadMasters();
- }, [toast, t]);
+ if (error) {
+ toast({
+ title: t('error.title'),
+ status: 'error',
+ isClosable: true,
+ position: 'bottom-right',
+ });
+ }
+ }, [error]);
return (
@@ -83,22 +70,21 @@ const Masters = () => {
- {loading && (
+ {isLoading && (
|
)}
- {!loading && masters.length === 0 && !error && (
+ {isSuccess && masters.length === 0 && (
{t('table.empty')}
|
)}
- {!loading &&
- !error &&
+ {isSuccess &&
masters.map((master, index) => (
))}
diff --git a/src/components/OrderItem/OrderItem.tsx b/src/components/OrderItem/OrderItem.tsx
index 5e677a2..d622187 100644
--- a/src/components/OrderItem/OrderItem.tsx
+++ b/src/components/OrderItem/OrderItem.tsx
@@ -3,8 +3,8 @@ import { Td, Tr, Link, Select } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
-import { MasterProps } from '../MasterItem/MasterItem';
import { getTimeSlot } from '../../lib';
+import { Master } from '../../models/api/master';
import { armService } from '../../api/arm';
const statuses = [
@@ -26,9 +26,9 @@ export type OrderProps = {
status?: GetArrItemType;
phone?: string;
location?: string;
- master: MasterProps;
+ master: Master;
notes: '';
- allMasters: MasterProps[];
+ allMasters: Master[];
id: string;
};
diff --git a/src/components/Orders/Orders.tsx b/src/components/Orders/Orders.tsx
index 99183bf..8a74f15 100644
--- a/src/components/Orders/Orders.tsx
+++ b/src/components/Orders/Orders.tsx
@@ -19,7 +19,7 @@ import OrderItem from '../OrderItem';
import { OrderProps } from '../OrderItem/OrderItem';
import { armService } from '../../api/arm';
import DateNavigator from '../DateNavigator';
-import { MasterProps } from '../MasterItem/MasterItem';
+import { Master } from '../../models/api/master';
const TABLE_HEADERS = [
'carNumber' as const,
@@ -41,7 +41,7 @@ const Orders = () => {
const toast = useToast();
const [orders, setOrders] = useState([]);
- const [allMasters, setAllMasters] = useState([]);
+ const [allMasters, setAllMasters] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentDate, setCurrentDate] = useState(new Date());
diff --git a/src/models/api/master.ts b/src/models/api/master.ts
new file mode 100644
index 0000000..1fa2e9d
--- /dev/null
+++ b/src/models/api/master.ts
@@ -0,0 +1,12 @@
+export interface Schedule {
+ id: string;
+ startWashTime: string;
+ endWashTime: string;
+}
+
+export type Master = {
+ id: string;
+ name: string;
+ phone: string;
+ schedule: Schedule[];
+};
diff --git a/stubs/api/admin.js b/stubs/api/admin.js
index 2f9e4eb..32cc16c 100644
--- a/stubs/api/admin.js
+++ b/stubs/api/admin.js
@@ -18,11 +18,13 @@ router.get('/', (req, res) => {
${generateRadioInput('masters', 'success')}
${generateRadioInput('masters', 'error')}
+ ${generateRadioInput('masters', 'empty')}