diff --git a/src/__data__/service/api.ts b/src/__data__/service/api.ts index 752f43d..f636621 100644 --- a/src/__data__/service/api.ts +++ b/src/__data__/service/api.ts @@ -1,34 +1,52 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { getConfigValue } from '@brojs/cli'; +import dayjs from 'dayjs'; import { Master } from '../../models/api/master'; +import { OrderProps } from '../../components/OrderItem/OrderItem'; -type SuccessResponse = { - success: true; - body: Body; -}; +import { extractBodyFromResponse } from './utils'; -type ErrorResponse = { - success: false; - message: string; -}; +export type UpdateMasterPayload = Required> & + Partial>; -type BaseResponse = SuccessResponse | ErrorResponse; +type UpdateOrderProps = Required> & + Partial> & { + master?: string; + }; export const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: getConfigValue('dry-wash.api') }), - tagTypes: ['Masters'], + tagTypes: ['Masters', 'Orders'], endpoints: (builder) => ({ getMasters: builder.query({ query: () => ({ url: '/arm/masters' }), - transformResponse: (response: BaseResponse) => { - if (response.success) { - return response.body; - } - }, + transformResponse: extractBodyFromResponse, providesTags: ['Masters'], }), + updateOrders: builder.mutation({ + query: ({ id, status, notes, master }) => ({ + url: `/order/${id}`, + method: 'PATCH', + body: { status, notes, master }, + }), + invalidatesTags: ['Orders'], + }), + getOrders: builder.query({ + query: ({ date }) => { + const startDate = dayjs(date).startOf('day').toISOString(); + const endDate = dayjs(date).endOf('day').toISOString(); + return { + url: '/arm/orders', + method: 'POST', + body: { startDate, endDate }, + }; + }, + transformResponse: extractBodyFromResponse, + providesTags: ['Orders'], + }), + addMaster: builder.mutation>({ query: (master) => ({ url: '/arm/masters', @@ -37,5 +55,29 @@ export const api = createApi({ }), invalidatesTags: ['Masters'], }), + deleteMaster: builder.mutation({ + query: ({ id }) => ({ + url: `/arm/masters/${id}`, + method: 'DELETE', + }), + invalidatesTags: ['Masters'], + }), + updateMaster: builder.mutation({ + query: ({ id, name, phone }) => ({ + url: `/arm/masters/${id}`, + method: 'PATCH', + body: { name, phone }, + }), + invalidatesTags: ['Masters'], + }), }), }); + +export const { + useGetMastersQuery, + useAddMasterMutation, + useDeleteMasterMutation, + useUpdateMasterMutation, + useGetOrdersQuery, + useUpdateOrdersMutation, +} = api; diff --git a/src/__data__/service/utils.ts b/src/__data__/service/utils.ts new file mode 100644 index 0000000..2ffe971 --- /dev/null +++ b/src/__data__/service/utils.ts @@ -0,0 +1,21 @@ +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; + +import { BaseResponse } from '../../models/api'; + +export const extractBodyFromResponse = (response: BaseResponse) => { + if (response.success) { + return response.body; + } +}; + +export const extractErrorMessageFromResponse = ({ + data, +}: FetchBaseQueryError) => { + if ( + typeof data === 'object' && + 'message' in data && + typeof data.message === 'string' + ) { + return data.message; + } +}; diff --git a/src/api/arm.ts b/src/api/arm.ts deleted file mode 100644 index 5c9e63f..0000000 --- a/src/api/arm.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { getConfigValue } from '@brojs/cli'; -import dayjs from 'dayjs'; - -enum ArmEndpoints { - ORDERS = '/arm/orders', - MASTERS = '/arm/masters', -} - -const armService = () => { - const endpoint = getConfigValue('dry-wash.api'); - - const fetchOrders = async ({ date }: { date: Date }) => { - const startDate = dayjs(date).startOf('day').toISOString(); - const endDate = dayjs(date).endOf('day').toISOString(); - - const response = await fetch(`${endpoint}${ArmEndpoints.ORDERS}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ startDate, endDate }), - }); - - if (!response.ok) { - throw new Error(`Failed to fetch orders: ${response.status}`); - } - - return await response.json(); - }; - - const fetchMasters = async () => { - const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}`); - - if (!response.ok) { - throw new Error(`Failed to fetch masters: ${response.status}`); - } - - return await response.json(); - }; - - const addMaster = async ({ - name, - phone, - }: { - name: string; - phone: string; - }) => { - const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name, phone }), - }); - - if (!response.ok) { - throw new Error(`Failed to fetch masters: ${response.status}`); - } - - return await response.json(); - }; - - const deleteMaster = async ({ id }: { id: string }) => { - const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}/${id}`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error(`Failed to fetch masters: ${response.status}`); - } - - return await response.json(); - }; - - const updateOrders = async ({ - id, - status, - notes, - masterId, - }: { - id: string; - status?: string; - notes?: string; - masterId?: string; - }) => { - const body = JSON.stringify({ status, notes, masterId }); - - const response = await fetch(`${endpoint}/order/${id}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch update masters: ${response.status}`); - } - - return await response.json(); - }; - - const updateMaster = async ({ - id, - name, - phone, - }: { - id: string; - name?: string; - phone?: string; - }) => { - const body = JSON.stringify({ name, phone }); - - const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}/${id}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch update masters: ${response.status}`); - } - - return await response.json(); - }; - - return { - fetchOrders, - fetchMasters, - addMaster, - deleteMaster, - updateMaster, - updateOrders, - }; -}; - -export { armService, ArmEndpoints }; diff --git a/src/components/Editable/Editable.tsx b/src/components/Editable/Editable.tsx index 9280bdf..61b6fad 100644 --- a/src/components/Editable/Editable.tsx +++ b/src/components/Editable/Editable.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Editable, EditableInput, @@ -14,22 +14,18 @@ import { import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons'; import { useTranslation } from 'react-i18next'; +import { useUpdateMasterMutation } from '../../__data__/service/api'; + interface EditableWrapperProps { value: string; - onSubmit: ({ - id, - name, - phone, - }: { - id: string; - name?: string; - phone?: string; - }) => Promise; as: 'phone' | 'name'; id: string; } -const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => { +const EditableWrapper = ({ value, as, id }: EditableWrapperProps) => { + const [updateMaster, { isError, isSuccess, error }] = + useUpdateMasterMutation(); + const { t } = useTranslation('~', { keyPrefix: 'dry-wash.arm.master.editable', }); @@ -40,11 +36,13 @@ const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => { const handleSubmit = async (newValue: string) => { if (currentValue === newValue) return; - try { - await onSubmit({ id, [as]: newValue }); + await updateMaster({ id, [as]: newValue }); - setCurrentValue(newValue); + setCurrentValue(newValue); + }; + useEffect(() => { + if (isSuccess) { toast({ title: 'Успешно!', description: 'Данные обновлены.', @@ -53,7 +51,11 @@ const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => { isClosable: true, position: 'top-right', }); - } catch (error) { + } + }, [isSuccess]); + + useEffect(() => { + if (isError) { toast({ title: 'Ошибка!', description: 'Не удалось обновить данные.', @@ -64,7 +66,7 @@ const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => { }); console.error('Ошибка при обновлении данных:', error); } - }; + }, [isError, error]); function EditableControls() { const { diff --git a/src/components/MasterActionsMenu/MasterActionsMenu.tsx b/src/components/MasterActionsMenu/MasterActionsMenu.tsx index 384c4aa..503a810 100644 --- a/src/components/MasterActionsMenu/MasterActionsMenu.tsx +++ b/src/components/MasterActionsMenu/MasterActionsMenu.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Menu, MenuButton, @@ -10,7 +10,7 @@ import { import { EditIcon } from '@chakra-ui/icons'; import { useTranslation } from 'react-i18next'; -import { armService } from '../../api/arm'; +import { useDeleteMasterMutation } from '../../__data__/service/api'; interface MasterActionsMenu { id: string; @@ -21,12 +21,17 @@ const MasterActionsMenu = ({ id }: MasterActionsMenu) => { keyPrefix: 'dry-wash.arm.master.table.actionsMenu', }); - const { deleteMaster } = armService(); + const [deleteMaster, { isSuccess, isError, error, isLoading }] = + useDeleteMasterMutation(); + const toast = useToast(); const handleClickDelete = async () => { - try { - await deleteMaster({ id }); + await deleteMaster({ id }); + }; + + useEffect(() => { + if (isSuccess) { toast({ title: 'Мастер удалён.', description: `Мастер с ID "${id}" успешно удалён.`, @@ -35,7 +40,11 @@ const MasterActionsMenu = ({ id }: MasterActionsMenu) => { isClosable: true, position: 'top-right', }); - } catch (error) { + } + }, [isSuccess]); + + useEffect(() => { + if (isError) { toast({ title: 'Ошибка удаления мастера.', description: 'Не удалось удалить мастера. Попробуйте ещё раз.', @@ -46,13 +55,15 @@ const MasterActionsMenu = ({ id }: MasterActionsMenu) => { }); console.error(error); } - }; + }, [isError]); return ( } as={IconButton} variant='outline' /> - {t('delete')} + + {t('delete')} + ); diff --git a/src/components/MasterDrawer/MasterDrawer.tsx b/src/components/MasterDrawer/MasterDrawer.tsx index fe790bb..b140e6c 100644 --- a/src/components/MasterDrawer/MasterDrawer.tsx +++ b/src/components/MasterDrawer/MasterDrawer.tsx @@ -18,10 +18,10 @@ import { import { useTranslation } from 'react-i18next'; import { PhoneIcon } from '@chakra-ui/icons'; -import { api } from '../../__data__/service/api'; +import { useAddMasterMutation } from '../../__data__/service/api'; const MasterDrawer = ({ isOpen, onClose }) => { - const [addMaster, { error, isSuccess }] = api.useAddMasterMutation(); + const [addMaster, { error, isSuccess }] = useAddMasterMutation(); const toast = useToast(); const [newMaster, setNewMaster] = useState({ name: '', phone: '' }); diff --git a/src/components/MasterItem/MasterItem.tsx b/src/components/MasterItem/MasterItem.tsx index 8df73de..80c5bdc 100644 --- a/src/components/MasterItem/MasterItem.tsx +++ b/src/components/MasterItem/MasterItem.tsx @@ -5,10 +5,8 @@ import { useTranslation } from 'react-i18next'; import MasterActionsMenu from '../MasterActionsMenu'; import { getTimeSlot } from '../../lib'; import EditableWrapper from '../Editable/Editable'; -import { armService } from '../../api/arm'; const MasterItem = ({ name, phone, id, schedule }) => { - const { updateMaster } = armService(); const { t } = useTranslation('~', { keyPrefix: 'dry-wash.arm.master', }); @@ -16,12 +14,7 @@ const MasterItem = ({ name, phone, id, schedule }) => { return ( - + {schedule?.length > 0 ? ( @@ -37,12 +30,7 @@ const MasterItem = ({ name, phone, id, schedule }) => { )} - + diff --git a/src/components/Masters/Masters.tsx b/src/components/Masters/Masters.tsx index a7aff65..e6a8cee 100644 --- a/src/components/Masters/Masters.tsx +++ b/src/components/Masters/Masters.tsx @@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'; import MasterItem from '../MasterItem'; import MasterDrawer from '../MasterDrawer'; -import { api } from '../../__data__/service/api'; +import { useGetMastersQuery } from '../../__data__/service/api'; const TABLE_HEADERS = [ 'name' as const, @@ -35,12 +35,7 @@ const Masters = () => { keyPrefix: 'dry-wash.arm.master', }); - const { - data: masters, - error, - isLoading, - isSuccess, - } = api.useGetMastersQuery(); + const { data: masters, error, isLoading, isSuccess } = useGetMastersQuery(); useEffect(() => { if (error) { diff --git a/src/components/OrderItem/OrderItem.tsx b/src/components/OrderItem/OrderItem.tsx index d622187..fd33955 100644 --- a/src/components/OrderItem/OrderItem.tsx +++ b/src/components/OrderItem/OrderItem.tsx @@ -5,7 +5,7 @@ import dayjs from 'dayjs'; import { getTimeSlot } from '../../lib'; import { Master } from '../../models/api/master'; -import { armService } from '../../api/arm'; +import { useUpdateOrdersMutation } from '../../__data__/service/api'; const statuses = [ 'pending' as const, @@ -54,8 +54,7 @@ const OrderItem = ({ allMasters, id, }: OrderProps) => { - const { updateOrders } = armService(); - + const [updateOrders] = useUpdateOrdersMutation(); const { t } = useTranslation('~', { keyPrefix: 'dry-wash.arm.order', }); @@ -72,16 +71,16 @@ const OrderItem = ({ if (selectedMaster) { setMaster(masterName); - updateOrders({ id, masterId: selectedMaster.id }); + updateOrders({ id, master: selectedMaster.id }); } else { console.error('Master not found'); } }; const handeChangeStatus = (e: ChangeEvent) => { - const status = e.target.value; + const status = e.target.value as OrderProps['status']; updateOrders({ id, status }); - setStatus(e.target.value as OrderProps['status']); + setStatus(status); }; return ( diff --git a/src/components/Orders/Orders.tsx b/src/components/Orders/Orders.tsx index 8a74f15..e4a37be 100644 --- a/src/components/Orders/Orders.tsx +++ b/src/components/Orders/Orders.tsx @@ -17,9 +17,11 @@ import dayjs from 'dayjs'; import OrderItem from '../OrderItem'; import { OrderProps } from '../OrderItem/OrderItem'; -import { armService } from '../../api/arm'; import DateNavigator from '../DateNavigator'; -import { Master } from '../../models/api/master'; +import { + useGetMastersQuery, + useGetOrdersQuery, +} from '../../__data__/service/api'; const TABLE_HEADERS = [ 'carNumber' as const, @@ -34,47 +36,41 @@ const Orders = () => { const { t } = useTranslation('~', { keyPrefix: 'dry-wash.arm.order', }); - - const { fetchOrders } = armService(); - const { fetchMasters } = armService(); - const toast = useToast(); - const [orders, setOrders] = useState([]); - const [allMasters, setAllMasters] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); const [currentDate, setCurrentDate] = useState(new Date()); + const { + data: orders, + isLoading: isOrdersLoading, + isSuccess: isOrdersSuccess, + isError: isOrdersError, + error: ordersError, + } = useGetOrdersQuery({ date: currentDate }); + + const { + data: masters, + isLoading: isMastersLoading, + isSuccess: isMastersSuccess, + isError: isMastersError, + error: mastersError, + } = useGetMastersQuery(); + + const isLoading = isOrdersLoading || isMastersLoading; + const isSuccess = isOrdersSuccess && isMastersSuccess; + const isError = isOrdersError || isMastersError; useEffect(() => { - const loadData = async () => { - setLoading(true); - setError(null); - - try { - const [ordersData, mastersData] = await Promise.all([ - fetchOrders({ date: currentDate }), - fetchMasters(), - ]); - - setOrders(ordersData.body); - setAllMasters(mastersData.body); - } catch (err) { - setError(err.message); - toast({ - title: t('error.title'), - status: 'error', - duration: 5000, - isClosable: true, - position: 'bottom-right', - }); - } finally { - setLoading(false); - } - }; - - loadData(); - }, [currentDate, toast, t]); + if (isError) { + toast({ + title: t('error.title'), + // description: errorMessage, + status: 'error', + duration: 5000, + isClosable: true, + position: 'bottom-right', + }); + } + }, [isError, ordersError, mastersError, toast, t]); return ( @@ -103,25 +99,24 @@ const Orders = () => { - {loading && ( + {isLoading && ( )} - {!loading && orders.length === 0 && !error && ( + {isSuccess && orders.length === 0 && ( {t('table.empty')} )} - {!loading && - !error && + {isSuccess && orders.map((order, index) => ( = { + success: true; + body: Body; +}; + +export type ErrorMessage = string; + +type ErrorResponse = { + success: false; + message: ErrorMessage; +}; + +export type BaseResponse = SuccessResponse | ErrorResponse; diff --git a/src/models/api/index.ts b/src/models/api/index.ts index f57351e..16ed054 100644 --- a/src/models/api/index.ts +++ b/src/models/api/index.ts @@ -1 +1,2 @@ -export * from './order'; \ No newline at end of file +export * from './common'; +export * from './order'; diff --git a/stubs/api/index.js b/stubs/api/index.js index f21ace4..8b8a7b4 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -53,7 +53,7 @@ router.delete('/arm/masters/:id', (req, res) => { ); }); -router.patch('/orders/:id', (req, res) => { +router.patch('/order/:id', (req, res) => { res .status(/error/.test(STUBS.orders) ? 500 : 200) .send(