ebanutsya

This commit is contained in:
2023-10-30 09:16:25 +03:00
parent cd89a70b17
commit 2a4479faac
13 changed files with 393 additions and 80 deletions

View File

@@ -30,6 +30,7 @@
"react-dom": "18.2.0",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.0",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-keyevent": "^0.3.1",
"react-native-keyevent-expo-config-plugin": "^1.0.49",
"react-native-modal": "^13.0.1",

38
src/api/assemblyApi.ts Normal file
View File

@@ -0,0 +1,38 @@
import apiClient from "./apiClient";
import {Assembly} from "../types/assembly";
const router = '/assembly';
const assemblyApi = {
create: async (orderId: number): Promise<{
ok: boolean,
message: string,
assemblyId: number,
statusCode: string
}> => {
let response = await apiClient.post(`${router}/create`, {orderId});
return response.data;
},
close: async (assemblyId: number): Promise<{ ok: boolean, message: string }> => {
let response = await apiClient.post(`${router}/close`, {assemblyId});
return response.data;
},
getActive: async (): Promise<Assembly> => {
let response = await apiClient.get(`${router}/getActive`);
return response.data;
},
hasActive: async (): Promise<{ has: boolean }> => {
let response = await apiClient.get(`${router}/hasActive`);
return response.data;
},
updateState: async (assemblyId: number, state: number): Promise<{ ok: boolean }> => {
let response = await apiClient.post(`${router}/updateState`, {assemblyId, state});
return response.data;
},
confirm: async (assemblyId: number): Promise<{ ok: boolean, message: string }> => {
let response = await apiClient.post(`${router}/confirm`, {assemblyId});
return response.data;
}
}
export default assemblyApi;

View File

@@ -10,6 +10,10 @@ const ordersApi = {
getOrdersBySupplierProduct: async (supplierProductId: number): Promise<Order[]> => {
let response = await apiClient.get(`${router}/getBySupplierProductId?supplierProductId=${supplierProductId}`);
return response.data;
},
getOrderById: async (orderId: number): Promise<Order> => {
let response = await apiClient.get(`${router}/getOrderById?orderId=${orderId}`);
return response.data;
}
}

BIN
src/assets/icons/cancel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1,69 @@
import {FC} from "react";
import {Props} from "react-native-paper";
import {StyleSheet, View, Image, TouchableOpacity} from "react-native";
import {BottomSheetModalProvider} from "@gorhom/bottom-sheet";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../../../redux/store";
import Modal from "react-native-modal";
import {background, blue} from "../../../css/colors";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import * as Progress from 'react-native-progress';
import ImageViewer from "react-native-image-zoom-viewer";
import {RFPercentage} from "react-native-responsive-fontsize";
import {closeImageZoomModal} from "../../../features/imageZoomModal/loadingModalSlice";
const ImageZoomModal: FC = () => {
const isVisible = useSelector((state: RootState) => state.imageZoomModal.isVisible);
const imageUrls = useSelector((state: RootState) => state.imageZoomModal.imageUrls);
const dispatch = useDispatch();
return (
<Modal isVisible={isVisible}>
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => {
dispatch(closeImageZoomModal())
}}>
<View style={styles.imageWrapper}>
<Image source={require('assets/icons/cancel.png')} style={styles.image}/>
</View>
</TouchableOpacity>
</View>
<ImageViewer
imageUrls={imageUrls.map(imageUrl => {
console.log(imageUrl)
return {
url: imageUrl
}
})}/>
</View>
</Modal>
)
}
const styles = StyleSheet.create({
container: {
alignSelf: "center",
backgroundColor: background,
borderRadius: responsiveWidth(2),
paddingBottom: responsiveHeight(5),
paddingHorizontal: responsiveWidth(5),
height: "95%",
width: "95%"
},
header: {
flexDirection: "row",
justifyContent: "flex-end",
padding: RFPercentage(2)
},
imageWrapper: {
height: responsiveHeight(4),
width: responsiveHeight(4),
},
image: {
height: "100%",
width: "100%",
resizeMode: "center"
}
})
export default ImageZoomModal;

View File

@@ -56,7 +56,7 @@ const OrderProductsList: FC<ListProps> = ({products, onSelected}) => {
const listStyles = StyleSheet.create({
container: {
backgroundColor: "white",
backgroundColor: 'white',
flex: 1
},
title: {

View File

@@ -76,7 +76,8 @@ const styles = StyleSheet.create({
borderRadius: 0,
borderTopRightRadius: responsiveWidth(1),
borderBottomRightRadius: responsiveWidth(1),
paddingHorizontal: responsiveWidth(5)
paddingHorizontal: responsiveWidth(5),
height:'100%'
},
scanImageWrapper: {
paddingHorizontal: responsiveWidth(1),

View File

@@ -1,12 +1,16 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import {Order} from "../../types/order";
import {Assembly, ASSEMBLY_STATE} from "../../types/assembly";
export interface AssemblyState {
order?: Order;
assembly?: Assembly;
localState?: ASSEMBLY_STATE
}
const initialState: AssemblyState = {
order: undefined,
localState: ASSEMBLY_STATE.NOT_STARTED
}
export const assembly = createSlice({
@@ -16,16 +20,47 @@ export const assembly = createSlice({
setOrder: (state, action) => {
state.order = action.payload;
},
setAssembly: (state, action: PayloadAction<Assembly>) => {
state.assembly = action.payload;
state.localState = action.payload.state;
},
startAssembly: (state) => {
if (!state.assembly) return;
state.assembly.createdAt = (new Date()).toDateString();
state.assembly.state = ASSEMBLY_STATE.ASSEMBLING_PRODUCTS;
state.localState = ASSEMBLY_STATE.ASSEMBLING_PRODUCTS;
},
endAssembly: (state) => {
if (!state.assembly) return;
state.assembly.endedAt = (new Date()).toDateString();
state.assembly.state = ASSEMBLY_STATE.ENDED;
state.localState = ASSEMBLY_STATE.ENDED;
state.assembly = undefined;
state.localState = ASSEMBLY_STATE.NOT_STARTED;
state.order = undefined;
},
confirmAssembly: (state) => {
if (!state.assembly) return;
state.assembly.state = ASSEMBLY_STATE.CONFIRMED;
state.localState = ASSEMBLY_STATE.CONFIRMED;
},
setAssembled: (state, action: PayloadAction<{ orderProductId: number }>) => {
if (!state.order || !state.order.products) return;
if (!state.order || !state.order.products || !state.assembly) return;
const orderProductId = action.payload.orderProductId;
state.order.products.forEach(product => {
if (product.databaseId === orderProductId) {
product.assembled = true;
}
});
// If all products is assembled
if (!state.order.products.find(product => !product.assembled)) {
state.assembly.state = ASSEMBLY_STATE.ALL_PRODUCTS_ASSEMBLED;
state.localState = ASSEMBLY_STATE.ALL_PRODUCTS_ASSEMBLED;
}
},
setShipped: (state, action: PayloadAction<{ orderProductId: number }>) => {
if (!state.order || !state.order.products) return;
@@ -41,5 +76,5 @@ export const assembly = createSlice({
}
})
export const {setOrder, setAssembled, setShipped} = assembly.actions;
export const {setOrder, setAssembled, setAssembly, startAssembly, endAssembly, confirmAssembly} = assembly.actions;
export default assembly.reducer;

View File

@@ -0,0 +1,30 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export interface ImageZoomModalState {
isVisible: boolean;
imageUrls: string[]
}
const initialState: ImageZoomModalState = {
isVisible: false,
imageUrls: []
}
export const imageZoomModalSlice = createSlice({
name: 'loadingModal',
initialState,
reducers: {
openImageZoomModal: (state) => {
state.isVisible = true
},
closeImageZoomModal: (state) => {
state.isVisible = false
},
setImages: (state, action: PayloadAction<string[]>) => {
state.imageUrls = action.payload;
}
}
})
export const {closeImageZoomModal, openImageZoomModal, setImages} = imageZoomModalSlice.actions;
export default imageZoomModalSlice.reducer;

View File

@@ -4,6 +4,7 @@ import authReducer from 'features/auth/authSlice';
import interfaceReducer from 'features/interface/interfaceSlice';
import scanModalReducer from 'features/scanModal/scanModalSlice';
import loadingModalReducer from 'features/loadingModal/loadingModalSlice';
import imageZoomModalReducer from 'features/imageZoomModal/loadingModalSlice';
import assemblyReducer from 'features/assembly/assemblySlice';
import printingReducer from 'features/printing/printingSlice';
import reprintModalReducer from 'features/reprintModal/reprintModalSlice';
@@ -17,7 +18,8 @@ export const store = configureStore({
loadingModal: loadingModalReducer,
assembly: assemblyReducer,
printing: printingReducer,
reprintModal: reprintModalReducer
reprintModal: reprintModalReducer,
imageZoomModal: imageZoomModalReducer
},
});

View File

@@ -14,21 +14,43 @@ import ScanModal from "../../components/SearchBar/ScanModal";
import {closeScanModal, setScannedData} from "../../features/scanModal/scanModalSlice";
import LoadingModal from "../../components/Modals/LoadingModal/LoadingModal";
import ReprintModal from "../../components/Modals/ReprintModal/ReprintModal";
import assemblyApi from "../../api/assemblyApi";
import {assembly, setAssembly, setOrder} from "../../features/assembly/assemblySlice";
import ordersApi from "../../api/ordersApi";
import ImageZoomModal from "../../components/Modals/ImageZoomModal/ImageZoomModal";
function CommonPage() {
const dim = useSelector((state: RootState) => state.interface.dim);
const isAuthorized = useSelector((state: RootState) => state.auth.isAuthorized);
const dispatch = useDispatch();
const loadSettings = async () => {
const loadSettings = async (): Promise<boolean> => {
const token = await SecureStore.getItemAsync('accessToken');
if (!token) return;
if (!token) return false;
dispatch(login({accessToken: token}));
return true;
}
const loadAssembly = async () => {
assemblyApi.hasActive().then(({has}) => {
if (!has) return;
assemblyApi.getActive().then(assembly => {
ordersApi.getOrderById(assembly.orderId).then(order => {
dispatch(setAssembly(assembly));
dispatch(setOrder(order));
})
})
})
}
const initialize = () => {
loadSettings().then(isSettingsLoaded => {
if (!isSettingsLoaded) return;
loadAssembly();
})
}
useEffect(() => {
loadSettings();
initialize();
}, []);
return (
@@ -39,6 +61,7 @@ function CommonPage() {
<LoadingModal/>
<ScanModal/>
<ReprintModal/>
<ImageZoomModal/>
<Toast config={toastConfig}/>
</View>

View File

@@ -1,4 +1,4 @@
import {View, Text, Image, StyleSheet, Button} from "react-native";
import {View, Text, Image, StyleSheet, Button, TouchableOpacity} from "react-native";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import DText from "../../components/DText/DText";
import {RFPercentage} from "react-native-responsive-fontsize";
@@ -16,14 +16,29 @@ import {useNavigation} from "@react-navigation/native";
import printingApi from "../../api/printingApi";
import PrintingService from "../../utils/PrintingService";
import OrderProductsList from "../../components/OrderCard/OrderProductsList";
import {setAssembled, setOrder} from "../../features/assembly/assemblySlice";
import {
confirmAssembly, endAssembly,
setAssembled,
setAssembly,
setOrder,
startAssembly
} from "../../features/assembly/assemblySlice";
import AcceptModal from "../../components/Modals/AcceptModal/AcceptModal";
import {Order} from "../../types/order";
import acceptModal from "../../components/Modals/AcceptModal/AcceptModal";
import printingService from "../../utils/PrintingService";
import ReprintModal from "../../components/Modals/ReprintModal/ReprintModal";
import {setData, setPrinterName} from "../../features/printing/printingSlice";
import {setPrinterName} from "../../features/printing/printingSlice";
import {openReprintModal} from "../../features/reprintModal/reprintModalSlice";
import {ASSEMBLY_STATE} from "../../types/assembly";
import {RenderTargetOptions} from "@shopify/flash-list";
import {createOnShouldStartLoadWithRequest} from "react-native-webview/lib/WebViewShared";
import assemblyApi from "../../api/assemblyApi";
import Toast from "react-native-toast-message";
import mainScreen from "../MainScreen/MainScreen";
import toast from "../../components/Toast/Toast";
import {openImageZoomModal, setImages} from "../../features/imageZoomModal/loadingModalSlice";
type AssemblyPeriod = {
started: Date;
@@ -65,39 +80,131 @@ type OrderScreenProps = {
const OrderScreen: FC<OrderScreenProps> = ({order}) => {
const navigator = useNavigation();
const dispatch = useDispatch();
const assembly = useSelector((state: RootState) => state.assembly.assembly);
const assemblyState = useSelector((state: RootState) => state.assembly.localState);
const [selectedProduct, setSelectedProduct] = useState(order.products[0]);
const [assemblyPeriod, setAssemblyPeriod] = useState<AssemblyPeriod>({started: new Date(), ended: new Date()})
const [acceptModalVisible, setAcceptModalVisible] = useState(false);
const [reprintModalVisible, setReprintModalVisible] = useState(false);
const [skipConfirmButtonVisible, setSkipConfirmButtonVisible] = useState(false);
const _confirmAssembly = async () => {
if (!assembly) return;
dispatch(setLoadingText('Подтверждение сборки...'))
dispatch(openLoadingModal());
assemblyApi.confirm(assembly.databaseId).then(({ok, message}) => {
dispatch(closeLoadingModal());
Toast.show({
type: ok ? 'success' : 'error',
text1: 'Подтверждение сборки',
text2: message
})
if (ok) {
dispatch(confirmAssembly());
const [assemblyStarted, setAssemblyStarted] = useState(false);
const [assemblyIntervalId, setAssemblyIntervalId] = useState(0);
const prettyPrintMilliseconds = (milliseconds: number) => {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
// Форматируем минуты и секунды, чтобы они всегда были двузначными
const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = String(seconds).padStart(2, '0');
return `${formattedMinutes}:${formattedSeconds}`;
} else {
setSkipConfirmButtonVisible(true);
}
const startAssembly = async () => {
setAssemblyStarted(true);
let intervalId = setInterval(() => {
setAssemblyPeriod(oldValue => ({...oldValue, ended: new Date()}))
}, 1000);
setAssemblyIntervalId(Number(intervalId))
};
})
}
const getButtons = () => {
switch (assemblyState) {
case ASSEMBLY_STATE.NOT_STARTED:
return (<BasicButton containerStyle={styles.buttonContainer}
onPress={() => setAcceptModalVisible(true)}
label={"Начать сборку"}/>)
case ASSEMBLY_STATE.ASSEMBLING_PRODUCTS:
return (<BasicButton
containerStyle={styles.buttonContainer}
label={"Отметить как собранный"}
disabled={selectedProduct.assembled}
onPress={() => {
dispatch(setAssembled({orderProductId: selectedProduct.databaseId}))
}}
/>)
case ASSEMBLY_STATE.ALL_PRODUCTS_ASSEMBLED:
return (
<>
<BasicButton
onPress={() => {
_confirmAssembly();
}}
containerStyle={styles.buttonContainer}
label={"Подтвердить сборку"}
/>
{skipConfirmButtonVisible && <BasicButton
onPress={() => {
dispatch(confirmAssembly());
}}
style={{backgroundColor: 'orange'}}
containerStyle={styles.buttonContainer}
label={"Пропустить подтверждение сборки"}
/>}
</>
)
case ASSEMBLY_STATE.CONFIRMED:
return (
<>
<BasicButton
onPress={() => printLabel()}
containerStyle={styles.buttonContainer}
label={"Печать этикетки"}
/>
<BasicButton
containerStyle={styles.buttonContainer}
style={{backgroundColor: 'green'}}
label={"Завершить сборку"}
onPress={() => {
if (!assembly) return;
assemblyApi.close(assembly.databaseId).then(({ok, message}) => {
Toast.show({
type: ok ? 'success' : 'error',
text1: 'Завершение сборки',
text2: message
});
dispatch(endAssembly());
})
}}/>
</>
)
case ASSEMBLY_STATE.ENDED:
return (
<BasicButton
containerStyle={styles.buttonContainer}
label={"Этот заказ уже собран"}
disabled
onPress={() => {
}}/>
)
}
}
const printLabel = () => {
if (!order) return;
dispatch(setLoadingText('Идет печать этикетки...'))
dispatch(openLoadingModal())
printingApi.getLabel(order.databaseId).then(pdfData => {
printingService.getInstance().printPdf('ozon', pdfData).then((response) => {
dispatch(closeLoadingModal());
if (response) return;
dispatch(setPrinterName({printerName: 'ozon'}));
dispatch(openReprintModal());
});
})
}
useEffect(() => {
let newSelectedProduct = order.products.find(o => o.databaseId == selectedProduct.databaseId);
if (!newSelectedProduct) return;
setSelectedProduct(newSelectedProduct);
}, [order]);
useEffect(() => {
dispatch(closeLoadingModal())
}, []);
if (!assembly) return;
assemblyApi.updateState(assembly.databaseId, Number(assemblyState)).then(ok => {
if (ok) return;
Toast.show({
type: 'error',
text1: 'Обновление состояния',
text2: 'Неудалось обновить состояние текущей сборки на сервере'
})
});
}, [assemblyState]);
return (
<View style={styles.container}>
<View style={styles.productsContainer}>
@@ -107,9 +214,16 @@ const OrderScreen: FC<OrderScreenProps> = ({order}) => {
setSelectedProduct(product)
}}/>
</View>
<View style={styles.imageWrapper}>
<TouchableOpacity style={styles.imageWrapper} onPress={()=>{
dispatch(setImages([selectedProduct.imageUrl]));
dispatch(openImageZoomModal());
}}>
<View style={{flex: 1}}>
<Image style={styles.image} source={{uri: selectedProduct.imageUrl}}/>
</View>
</TouchableOpacity>
</View>
<View style={styles.contentContainer}>
<View style={styles.dataContainer}>
@@ -124,49 +238,29 @@ const OrderScreen: FC<OrderScreenProps> = ({order}) => {
<DText>Номер товара: {0}</DText>
<DText>{}</DText>
<DTitle style={styles.contentTitle}>Сборка</DTitle>
<DText>Затрачено
времени: {prettyPrintMilliseconds(assemblyPeriod.ended.getTime() - assemblyPeriod.started.getTime())}</DText>
</View>
<View style={styles.buttonsContainer}>
{assemblyStarted ? <>
<BasicButton
containerStyle={styles.buttonContainer}
label={"Отметить как собранный"}
disabled={selectedProduct.assembled}
onPress={() => {
dispatch(setAssembled({orderProductId: selectedProduct.databaseId}))
}}
/>
<BasicButton
onPress={() => {
if (!order) return;
dispatch(setLoadingText('Идет печать этикетки...'))
dispatch(openLoadingModal())
printingApi.getLabel(order.databaseId).then(pdfData => {
printingService.getInstance().printPdf('ozon', pdfData).then((response) => {
dispatch(closeLoadingModal());
if (response) return;
dispatch(setPrinterName({printerName: 'ozon'}));
dispatch(openReprintModal());
});
})
}}
containerStyle={styles.buttonContainer}
label={"Печать этикетки"}
disabled={Boolean(order.products.find(product => !product.assembled))}/>
</>
: <BasicButton containerStyle={styles.buttonContainer}
onPress={() => setAcceptModalVisible(true)}
label={"Начать сборку"}/>}
{getButtons()}
</View>
</View>
<AcceptModal visible={acceptModalVisible}
text={`Вы уверены что хотите начать сборку заказа ${order.orderNumber}`}
onAccepted={() => {
setAcceptModalVisible(false);
startAssembly();
assemblyApi.create(order.databaseId).then(creationResult => {
console.log(creationResult.message)
Toast.show({
type: creationResult.ok ? 'success' : 'error',
text1: 'Создание сборки',
text2: creationResult.message
});
if (!creationResult.ok) return;
assemblyApi.getActive().then(activeAssembly => {
dispatch(setAssembly(activeAssembly));
dispatch(startAssembly())
})
})
}}
onRefused={() => {
setAcceptModalVisible(false);

16
src/types/assembly.ts Normal file
View File

@@ -0,0 +1,16 @@
export enum ASSEMBLY_STATE {
NOT_STARTED,
ASSEMBLING_PRODUCTS,
ALL_PRODUCTS_ASSEMBLED,
CONFIRMED,
ENDED
}
export type Assembly = {
databaseId: number;
createdAt: string | null;
endedAt: string | null;
orderId: number;
isActive: boolean;
state: ASSEMBLY_STATE
}