ebanutsya

This commit is contained in:
2023-10-27 06:03:46 +03:00
parent 4f25112b5e
commit 37d3fb5a78
34 changed files with 831 additions and 17841 deletions

View File

@@ -1,5 +1,8 @@
{
"expo": {
"plugins": [
"react-native-keyevent-expo-config-plugin"
],
"name": "Assemblr",
"slug": "Assemblr",
"version": "1.0.0",
@@ -21,10 +24,16 @@
"adaptiveIcon": {
"foregroundImage": "./src/assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"package": "com.anonymous.Assemblr"
},
"web": {
"favicon": "./src/assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "44593e40-e504-4784-9e23-3cc90a6c1f74"
}
}
}
}

18
eas.json Normal file
View File

@@ -0,0 +1,18 @@
{
"build": {
"preview": {
"android": {
"buildType": "apk"
}
},
"preview2": {
"android": {
"gradleCommand": ":app:assembleRelease"
}
},
"preview3": {
"developmentClient": true
},
"production": {}
}
}

View File

@@ -1,6 +1,11 @@
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
module.exports = {
// Получаем конфигурацию по умолчанию от Expo
const defaultConfig = getDefaultConfig(__dirname);
// Ваши настройки
const customConfig = {
resolver: {
extraNodeModules: new Proxy({}, {
get: (target, name) => path.join(process.cwd(), `src/${name}`)
@@ -10,3 +15,16 @@ module.exports = {
path.resolve(__dirname, 'src')
],
};
// Объединяем два конфига
module.exports = {
...defaultConfig,
resolver: {
...defaultConfig.resolver,
...customConfig.resolver
},
watchFolders: [
...defaultConfig.watchFolders,
...customConfig.watchFolders
]
};

17594
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,39 +4,49 @@
"main": "AppEntry.ts",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"resolutions": {
"react-native-reanimated": "3.3.0"
},
"dependencies": {
"@expo/webpack-config": "^19.0.0",
"@gorhom/bottom-sheet": "^4",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7",
"@reduxjs/toolkit": "^1.9.5",
"@rneui/base": "^4.0.0-rc.8",
"@rneui/base": "^4.0.0-rc.7",
"@rneui/themed": "^4.0.0-rc.8",
"@shopify/flash-list": "1.4.3",
"axios": "^1.5.0",
"babel-plugin-module-resolver": "^5.0.0",
"expo": "~49.0.8",
"expo-secure-store": "~12.3.1",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.5",
"react-native-gesture-handler": "^2.13.2",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.0",
"react-native-keyevent": "^0.3.1",
"react-native-keyevent-expo-config-plugin": "^1.0.49",
"react-native-modal": "^13.0.1",
"react-native-paper": "^5.10.6",
"react-native-reanimated": "~3.3.0",
"react-native-radio-buttons-group": "^3.0.5",
"react-native-reanimated": "3.3.0",
"react-native-responsive-dimensions": "^3.1.1",
"react-native-responsive-fontsize": "^0.5.1",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-toast-message": "^2.1.7",
"react-native-vector-icons": "^10.0.0",
"react-native-web": "~0.19.6",
"react-native-webview": "13.2.2",
"react-redux": "^8.1.2",
"redux": "^4.2.1"
"redux": "^4.2.1",
"rn-openapp": "^2.1.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@@ -7,6 +7,7 @@ import React from "react";
import CommonPage from "./screens/CommonPage/CommonPage";
import {BottomSheetModalProvider} from "@gorhom/bottom-sheet";
import {GestureHandlerRootView} from "react-native-gesture-handler";
import Toast from "react-native-toast-message";
export default function App() {
@@ -21,6 +22,7 @@ export default function App() {
<GestureHandlerRootView style={{flex: 1}}>
<BottomSheetModalProvider>
<CommonPage/>
</BottomSheetModalProvider>
</GestureHandlerRootView>

View File

@@ -1,12 +1,15 @@
import axios, {AxiosHeaders, AxiosRequestConfig, AxiosRequestHeaders, InternalAxiosRequestConfig} from 'axios';
import * as SecureStore from 'expo-secure-store';
import {useDispatch} from "react-redux";
import {logout} from "../features/auth/authSlice";
import {store} from "../redux/store";
const apiClient = axios.create({
baseURL: 'http://192.168.1.101:5000',
});
apiClient.interceptors.request.use(async (config) => {
const accessToken = await SecureStore.getItemAsync('access_token');
const accessToken = await SecureStore.getItemAsync('accessToken');
if (!config.headers) {
config.headers = new AxiosHeaders();
}
@@ -14,12 +17,22 @@ apiClient.interceptors.request.use(async (config) => {
config.headers.set('Authorization', `Bearer ${accessToken}`, true);
}
config.validateStatus = (status) => {
return true
if (status == 401) {
SecureStore.deleteItemAsync('accessToken');
store.dispatch(logout());
}
return true;
};
return config;
}, function (error) {
return Promise.reject(error);
if (error.response && error.response.status === 401) {
console.log("очко")
}
// return Promise.reject(error);
});
export default apiClient;

13
src/api/barcodeApi.ts Normal file
View File

@@ -0,0 +1,13 @@
import {SupplierProduct} from "../types/supplierProduct";
import apiClient from "./apiClient";
const router = '/barcode';
const barcodeApi = {
searchProducts: async (barcode: string): Promise<SupplierProduct[]> => {
let response = await apiClient.get(`${router}/searchProducts?barcode=${barcode}`);
return response.data;
}
}
export default barcodeApi;

16
src/api/ordersApi.ts Normal file
View File

@@ -0,0 +1,16 @@
import apiClient from "./apiClient";
import {Order} from "../types/order";
const router = '/orders';
const ordersApi = {
getOrders: async (lastId: number) => {
},
getOrdersBySupplierProduct: async (supplierProductId: number): Promise<Order[]> => {
let response = await apiClient.get(`${router}/getBySupplierProductId?supplierProductId=${supplierProductId}`);
return response.data;
}
}
export default ordersApi;

12
src/api/userApi.ts Normal file
View File

@@ -0,0 +1,12 @@
import apiClient from "./apiClient";
const userApi = {
login: async (login: string, password: string): Promise<{ accessToken: string, ok: boolean }> => {
let response = await apiClient.post('/auth/login', {login, password});
return response.data;
},
test: async () => {
await apiClient.post('/auth/protected');
}
}
export default userApi;

View File

@@ -0,0 +1,9 @@
import {FC} from "react";
import {responsiveHeight} from "react-native-responsive-dimensions";
import {View} from "react-native";
const FlashListSeparator: FC = () => {
return (<View
style={{flex: 1, height: responsiveHeight(2)}}></View>);
}
export default FlashListSeparator;

View File

@@ -0,0 +1,97 @@
import React, {FC} from "react";
import {SupplierProduct} from "../../../types/supplierProduct";
import {Image, StyleSheet, TouchableOpacity, View} from "react-native";
import DTitle from "../../DTitle/DTitle";
import DText from "../../DText/DText";
import {RFPercentage} from "react-native-responsive-fontsize";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
type Props = {
product: SupplierProduct;
onPress: (product: SupplierProduct) => void;
}
const SelectProductElement: FC<Props> = React.memo(({product, onPress}) => {
return (
<TouchableOpacity onPress={() => {
onPress(product)
}}>
<View style={styles.container}>
<View style={styles.imageWrapper}>
<Image style={styles.image} source={{uri: product.thumbUrl}}/>
</View>
<View style={styles.description}>
<View style={styles.descriptionContent}>
<DText>{product.productName}</DText>
<DText style={{textAlign: "justify"}}>{}</DText>
{product.supplierName &&
<DText>Поставщик: {product.supplierName}</DText>
}
{product.supplierArticle &&
<DText>Артикул: {product.supplierArticle}</DText>
}
{product.inBlock &&
<DText>В блоке: {product.inBlock}</DText>
}
{product.shelfNumber &&
<DText>Номер полки: {product.shelfNumber}</DText>
}
</View>
</View>
</View>
</TouchableOpacity>
)
})
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
display: "flex",
borderRadius: RFPercentage(3),
flexDirection: "row",
padding: RFPercentage(2),
flex: 1,
},
imageWrapper: {
width: responsiveWidth(20),
},
image: {
width: "100%",
height: "100%",
},
description: {
// backgroundColor: "red",
flex: 1,
display: "flex",
flexDirection: "column",
paddingLeft: responsiveWidth(3),
gap: 0,
},
title: {
marginBottom: responsiveHeight(0.5),
flexDirection: "row",
alignItems: "center"
},
titleImage: {
width: responsiveWidth(4),
height: responsiveHeight(4),
resizeMode: "center",
marginLeft: responsiveHeight(1)
},
descriptionContent: {
// backgroundColor: "green",
flex: 1,
},
descriptionStatus: {
alignSelf: "flex-end",
// backgroundColor: "blue",
marginRight: responsiveWidth(2),
}
});
export default SelectProductElement;

View File

@@ -0,0 +1,59 @@
import {FC} from "react";
import {FlatList, StyleSheet, View} from "react-native";
import Modal from "react-native-modal";
import {FlashList} from "@shopify/flash-list";
import SelectProductElement from "./SelectProductElement";
import {SupplierProduct} from "../../../types/supplierProduct";
import FlashListSeparator from "../../FlashListSeparator/FlashListSeparator";
import DTitle from "../../DTitle/DTitle";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import {background} from "../../../css/colors";
type Props = {
visible: boolean;
products: SupplierProduct[];
onSelected: (product: SupplierProduct) => void
}
const SelectProductModal: FC<Props> = ({visible, products, onSelected}) => {
return (
<Modal isVisible={visible}>
<View style={styles.container}>
<DTitle style={styles.title}>К штрихкоду привязано несколько
товаров, выберите конкретный</DTitle>
<FlatList
keyboardShouldPersistTaps={"never"}
data={products}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
keyExtractor={(product) => product.supplierProductId.toString()}
renderItem={(product) => <SelectProductElement product={product.item} onPress={onSelected}/>}
ItemSeparatorComponent={FlashListSeparator}
>
</FlatList>
</View>
</Modal>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: background,
borderRadius: responsiveWidth(1),
paddingHorizontal: responsiveWidth(5),
paddingVertical: responsiveHeight(5),
rowGap: responsiveHeight(3),
marginVertical:responsiveHeight(10)
},
title: {
textAlign: "center"
},
listContainer: {
flex: 1
}
})
export default SelectProductModal;

View File

@@ -0,0 +1,113 @@
import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
import {BottomSheetModal} from "@gorhom/bottom-sheet";
import {disableDim, enableDim} from "../../../features/interface/interfaceSlice";
import {StyleSheet, View} from "react-native";
import BasicButton from "../../BasicButton/BasicButton";
import {useDispatch} from "react-redux";
import {RFPercentage} from "react-native-responsive-fontsize";
import RadioGroup from 'react-native-radio-buttons-group';
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import {blue} from "../../../css/colors";
export type SortingModalHandles = {
present: () => void;
dismiss: () => void;
}
export type SortingModalElement = {
id: string;
label: string;
value: string;
}
type Props = {
onChange: (sortingValue: string) => void;
onClose: () => void;
elements: SortingModalElement[];
defaultElementId?: string;
};
const createRadioButton = (element: SortingModalElement) => {
return {
id: element.id,
label: element.label,
value: element.value,
size: RFPercentage(5),
color: blue,
labelStyle: {
fontSize: responsiveWidth(3),
fontWeight: "500" as const
},
borderSize: RFPercentage(0.5)
};
}
const SortingModal = forwardRef<SortingModalHandles, Props>((props: Props, ref) => {
const {elements, onChange, onClose, defaultElementId = ""} = props;
const snapPoints = useMemo(() => ['40%', '40%'], []);
const dispatch = useDispatch();
const modalRef = useRef<BottomSheetModal>(null);
const dismiss = () => {
if (!modalRef.current) return;
dispatch(disableDim());
modalRef.current.dismiss();
onClose();
}
const present = () => {
if (!modalRef.current) return;
modalRef.current.present();
dispatch(enableDim());
}
useImperativeHandle(ref, () => ({
present: present,
dismiss: dismiss
}));
const [selectedId, setSelectedId] = useState<string>(defaultElementId);
useEffect(() => {
onChange(selectedId);
}, [selectedId]);
return (
<BottomSheetModal
ref={modalRef}
snapPoints={snapPoints}
onDismiss={() => {
dispatch(disableDim());
}}>
<View style={styles.container}>
<View style={styles.content}>
<RadioGroup selectedId={selectedId}
onPress={setSelectedId}
containerStyle={styles.radioButtons}
radioButtons={elements.map(createRadioButton)}/>
</View>
<BasicButton label={"Закрыть"} style={styles.button} onPress={() => {
dismiss();
}}/>
</View>
</BottomSheetModal>
);
});
const styles = StyleSheet.create({
container: {
width: "100%",
height: "100%",
flex: 1,
padding: RFPercentage(3),
flexDirection: "column",
justifyContent: "space-between"
},
radioButtons: {
alignItems: "flex-start"
},
content: {},
button: {
marginTop: "auto"
},
});
export default SortingModal;

View File

@@ -1,26 +1,18 @@
import {FC, useState} from "react";
import React, {FC, useState} from "react";
import {GestureResponderEvent, Image, StyleSheet, TouchableOpacity, View} from "react-native";
import {RFPercentage} from "react-native-responsive-fontsize";
import DText from "../DText/DText";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import DTitle from "../DTitle/DTitle";
import {Order} from "../../types/order";
type Props = {
onPress?: (event: GestureResponderEvent) => void
order: Order
}
const OrderCard: FC<Props> = ({onPress}) => {
const OrderCard: FC<Props> = React.memo(({onPress, order}) => {
const [order, setOrder] = useState({
article: 183658,
imageUrl: "https://421421.selcdn.ru/denco/996956/thumbzoom/h0b41036e5dc84a88b3dd344a46ab33edt.jpg-640x640.jpg",
orderNumber: "93757290-0104-7",
productName: "Фигурки животных «Собачки» 258, 5-7 см., статичные / 12 шт.",
supplierName: "simaland",
marketplaceName: "Wildberries РЕНТА",
sellerName: "DENCO",
assembled: false
});
return (
<TouchableOpacity onPress={() => {
@@ -54,7 +46,7 @@ const OrderCard: FC<Props> = ({onPress}) => {
</TouchableOpacity>
)
}
})
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
@@ -63,6 +55,7 @@ const styles = StyleSheet.create({
height: responsiveHeight(20),
flexDirection: "row",
padding: RFPercentage(2),
flex: 1
},
imageWrapper: {
width: responsiveWidth(30),

View File

@@ -5,6 +5,7 @@ import {RFPercentage} from "react-native-responsive-fontsize";
import Modal from "react-native-modal";
import BasicButton from "../BasicButton/BasicButton";
import DText from "../DText/DText";
import {useDispatch} from "react-redux";
type Props = {
visible: boolean
@@ -16,14 +17,21 @@ const ScanModal: FC<Props> = ({visible, onCancelButtonPress, onChanged}) => {
useEffect(() => {
if (visible) inputRef.current?.focus();
}, [visible]);
return (
<Modal isVisible={visible}>
<View style={styles.container}>
<DText style={styles.text}>Наведите сканер на штрихкод товара или заказа</DText>
<BasicButton onPress={onCancelButtonPress} style={styles.cancelButton} label={"Отмена"}/>
<TextInput onEndEditing={(e) => {
onChanged(e.nativeEvent.text);
}} style={styles.pseudoInput} ref={inputRef}/>
<TextInput
onEndEditing={(e) => {
onChanged(e.nativeEvent.text);
}}
style={styles.pseudoInput}
ref={inputRef}
autoFocus={true}
showSoftInputOnFocus={false}
/>
</View>
</Modal>
)

View File

@@ -1,27 +1,41 @@
import React, {FC, useRef, useState} from "react";
import React, {FC, useEffect, useRef, useState} from "react";
import {Image, StyleSheet, TextInput, TouchableOpacity, View} from "react-native";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import {RFPercentage} from "react-native-responsive-fontsize";
import BasicButton from "../BasicButton/BasicButton";
import ScanModal from "./ScanModal";
import telegramAuthButton from "../TelegramAuthButton/TelegramAuthButton";
import SelectProductModal from "../Modals/SelectProductModal/SelectProductModal";
import {SupplierProduct} from "../../types/supplierProduct";
import barcodeApi from "../../api/barcodeApi";
import {useDispatch, useSelector} from "react-redux";
import {openScanModal} from "../../features/scanModal/scanModalSlice";
import {RootState} from "../../redux/store";
type Props = {
onSearch: (text: string) => void;
onSupplierProductSelected?: (supplierProduct: SupplierProduct) => void
}
const SearchBar: FC<Props> = ({onSearch}) => {
const [isScanModalVisible, setIsScanModalVisible] = useState<boolean>(false);
const SearchBar: FC<Props> = ({onSearch, onSupplierProductSelected}) => {
// const [isScanModalVisible, setIsScanModalVisible] = useState<boolean>(false);
// const [, setSelectProductModalVisible] = useState(false);
const dispatch = useDispatch();
const [searchInput, setSearchInput] = useState<string>("");
const textInputRef = useRef<TextInput>(null);
const [products, setProducts] = useState<SupplierProduct[]>([]);
const scannedData = useSelector((state: RootState) => state.scanModal.scannedData);
useEffect(() => {
if (!scannedData) return;
barcodeApi.searchProducts(scannedData).then(setProducts);
}, [scannedData]);
const selectProductModalVisible = products.length > 0;
return (
<View style={styles.container}>
<ScanModal
onChanged={(text) => {
setIsScanModalVisible(false);
onSearch(text);
}}
onCancelButtonPress={() => setIsScanModalVisible(false)}
visible={isScanModalVisible}/>
<SelectProductModal visible={selectProductModalVisible} products={products} onSelected={(product) => {
if (onSupplierProductSelected) onSupplierProductSelected(product);
setProducts([]);
}}/>
<BasicButton onPress={() => {
onSearch(searchInput);
if (textInputRef.current) {
@@ -29,7 +43,9 @@ const SearchBar: FC<Props> = ({onSearch}) => {
}
}} style={styles.scanButton} label={"Поиск"}/>
<View style={styles.scanImageWrapper}>
<TouchableOpacity onPress={() => setIsScanModalVisible(true)}>
<TouchableOpacity onPress={() => {
dispatch(openScanModal());
}}>
<Image style={styles.scanImage} source={require('assets/icons/scan.png')}/>
</TouchableOpacity>
</View>

View File

@@ -0,0 +1,30 @@
import Toast, {BaseToast, ErrorToast, ToastConfig} from 'react-native-toast-message';
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
const toastConfig: ToastConfig = {
success: (props) => (
<BaseToast
{...props}
text1Style={{fontSize: responsiveWidth(3)}}
text2Style={{fontSize: responsiveWidth(3)}}
style={{
width: responsiveWidth(90),
height: responsiveHeight(10),
borderLeftColor: "green"
}}
/>),
error: (props) => (
<BaseToast
{...props}
text1Style={{fontSize: responsiveWidth(3)}}
text2Style={{fontSize: responsiveWidth(3)}}
style={{
width: responsiveWidth(90),
height: responsiveHeight(10),
borderLeftColor: "red"
}}
/>),
}
export default toastConfig;

View File

@@ -1,119 +1,32 @@
import {createSlice, createAsyncThunk, PayloadAction, ThunkDispatch} from '@reduxjs/toolkit';
import apiClient from 'api/apiClient';
import {createSlice} from "@reduxjs/toolkit";
import * as SecureStore from 'expo-secure-store';
import {AppDispatch} from "../../redux/store";
import {useDispatch} from "react-redux";
import {AxiosError} from "axios";
import {RejectedAction} from "@reduxjs/toolkit/dist/query/core/buildThunks";
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import {EndpointBuilder} from "@reduxjs/toolkit/dist/query/endpointDefinitions";
import {useDebugValue} from "react";
interface AuthState {
accessToken: string | null;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
errorMessage: string | null;
isAuthenticated: boolean;
export interface AuthState {
isAuthorized: boolean;
accessToken?: string;
}
const initialState: AuthState = {
accessToken: null,
status: 'idle',
errorMessage: null,
isAuthenticated: false
};
isAuthorized: false,
accessToken: undefined,
}
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials: { login: string; password: string }, {rejectWithValue}) => {
try {
const response = await apiClient.post<{ access_token: string }>('/auth/login', credentials);
if (response.status == 401) {
throw Error("Invalid credentials");
}
return response.data.access_token;
} catch (error: any) {
if (error instanceof AxiosError)
return rejectWithValue('Server response error');
else if (error instanceof Error)
return rejectWithValue('Invalid credentials');
return rejectWithValue('Unexpected error')
}
}
);
export const loadToken = createAsyncThunk<
void,
undefined,
{
dispatch: AppDispatch // Тип для dispatch
}
>('auth/loadToken', async (_, {dispatch}) => {
try {
const token = await SecureStore.getItemAsync('access_token');
if (token) {
dispatch(authSlice.actions.setToken(token));
}
} catch (error) {
console.error("Couldn't read token", error);
}
});
export const logoutUser = createAsyncThunk<
void,
undefined,
{
dispatch: AppDispatch // Тип для dispatch
}
>('auth/loadToken', async (_, {dispatch}) => {
try {
await SecureStore.deleteItemAsync('access_token');
dispatch(authSlice.actions.logout())
} catch (error) {
console.error("Couldn't read token", error);
}
});
const authSlice = createSlice({
name: 'auth',
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setToken: (state, action: PayloadAction<string>) => {
state.accessToken = action.payload;
state.isAuthenticated = true;
state.status = "succeeded";
login: (state, action) => {
state.isAuthorized = true;
state.accessToken = action.payload.accessToken;
},
logout: (state) => {
state.accessToken = null;
state.status = "idle";
state.isAuthenticated = false
state.errorMessage = null;
state.isAuthorized = false;
state.accessToken = undefined
}
},
extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => {
state.accessToken = "";
state.status = 'loading';
state.errorMessage = null;
state.isAuthenticated = false;
}).addCase(loginUser.fulfilled, (state, action: PayloadAction<string>) => {
let accessToken = action.payload;
state.accessToken = accessToken;
state.status = 'succeeded';
state.errorMessage = null
state.isAuthenticated = true;
SecureStore.setItemAsync('access_token', accessToken);
}).addCase(loginUser.rejected, (state, action) => {
if (action.payload)
state.errorMessage = action.payload as string;
state.status = "failed";
state.isAuthenticated = false;
state.accessToken = "";
})
},
}
});
export const {login, logout} = authSlice.actions;
export default authSlice.reducer;
export default authSlice.reducer;

View File

@@ -0,0 +1,119 @@
import {createSlice, createAsyncThunk, PayloadAction, ThunkDispatch} from '@reduxjs/toolkit';
import apiClient from 'api/apiClient';
import * as SecureStore from 'expo-secure-store';
import {AppDispatch} from "../../redux/store";
import {useDispatch} from "react-redux";
import {AxiosError} from "axios";
import {RejectedAction} from "@reduxjs/toolkit/dist/query/core/buildThunks";
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import {EndpointBuilder} from "@reduxjs/toolkit/dist/query/endpointDefinitions";
interface AuthState {
accessToken: string | null;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
errorMessage: string | null;
isAuthenticated: boolean;
}
const initialState: AuthState = {
accessToken: null,
status: 'idle',
errorMessage: null,
isAuthenticated: false
};
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials: { login: string; password: string }, {rejectWithValue}) => {
try {
const response = await apiClient.post<{ access_token: string }>('/auth/login', credentials);
if (response.status == 401) {
throw Error("Invalid credentials");
}
return response.data.access_token;
} catch (error: any) {
if (error instanceof AxiosError)
return rejectWithValue('Server response error');
else if (error instanceof Error)
return rejectWithValue('Invalid credentials');
return rejectWithValue('Unexpected error')
}
}
);
export const loadToken = createAsyncThunk<
void,
undefined,
{
dispatch: AppDispatch // Тип для dispatch
}
>('auth/loadToken', async (_, {dispatch}) => {
try {
const token = await SecureStore.getItemAsync('access_token');
if (token) {
dispatch(authSlice.actions.setToken(token));
}
} catch (error) {
console.error("Couldn't read token", error);
}
});
export const logoutUser = createAsyncThunk<
void,
undefined,
{
dispatch: AppDispatch // Тип для dispatch
}
>('auth/loadToken', async (_, {dispatch}) => {
try {
await SecureStore.deleteItemAsync('access_token');
dispatch(authSlice.actions.logout())
} catch (error) {
console.error("Couldn't read token", error);
}
});
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setToken: (state, action: PayloadAction<string>) => {
state.accessToken = action.payload;
state.isAuthenticated = true;
state.status = "succeeded";
},
logout: (state) => {
state.accessToken = null;
state.status = "idle";
state.isAuthenticated = false
state.errorMessage = null;
}
},
extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => {
state.accessToken = "";
state.status = 'loading';
state.errorMessage = null;
state.isAuthenticated = false;
}).addCase(loginUser.fulfilled, (state, action: PayloadAction<string>) => {
let accessToken = action.payload;
state.accessToken = accessToken;
state.status = 'succeeded';
state.errorMessage = null
state.isAuthenticated = true;
SecureStore.setItemAsync('access_token', accessToken);
}).addCase(loginUser.rejected, (state, action) => {
if (action.payload)
state.errorMessage = action.payload as string;
state.status = "failed";
state.isAuthenticated = false;
state.accessToken = "";
})
},
});
export default authSlice.reducer;

View File

@@ -0,0 +1,30 @@
import {createSlice} from "@reduxjs/toolkit";
export interface ScanModalState {
isVisible: boolean;
scannedData?: string
}
const initialState: ScanModalState = {
isVisible: false,
scannedData: undefined
}
export const scanModalSlice = createSlice({
name: 'scanModal',
initialState,
reducers: {
openScanModal: (state) => {
state.isVisible = true
},
closeScanModal: (state) => {
state.isVisible = false
},
setScannedData: (state, action) => {
state.scannedData = action.payload;
}
}
})
export const {openScanModal, closeScanModal, setScannedData} = scanModalSlice.actions;
export default scanModalSlice.reducer;

View File

@@ -2,12 +2,14 @@ import {configureStore} from '@reduxjs/toolkit';
import authReducer from 'features/auth/authSlice';
import interfaceReducer from 'features/interface/interfaceSlice';
import scanModalReducer from 'features/scanModal/scanModalSlice';
import {useDispatch} from "react-redux";
export const store = configureStore({
reducer: {
auth: authReducer,
interface: interfaceReducer
interface: interfaceReducer,
scanModal: scanModalReducer
},
});

View File

@@ -1,6 +1,5 @@
import {Button, Text, View} from "react-native";
import {useAppDispatch} from "../../redux/store";
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
import * as process from "process";
function BarcodeScreen() {

View File

@@ -1,6 +1,6 @@
import {Button, Text, View} from "react-native";
import {useAppDispatch} from "../../redux/store";
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice.ts.back";
import * as process from "process";
function BoxScreen() {

View File

@@ -2,18 +2,48 @@ import {SafeAreaView, StyleSheet, View} from "react-native";
import LoginScreen from "../LoginScreen/LoginScreen";
import MainScreen from "../MainScreen/MainScreen";
import SearchBar from "../../components/SearchBar/SearchBar";
import React from "react";
import React, {useEffect} from "react";
import {background} from "../../css/colors";
import {useSelector} from "react-redux";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../../redux/store";
import {login} from "../../features/auth/authSlice";
import * as SecureStore from 'expo-secure-store';
import Toast from "react-native-toast-message";
import toastConfig from "../../components/Toast/Toast";
import ScanModal from "../../components/SearchBar/ScanModal";
import {closeScanModal, setScannedData} from "../../features/scanModal/scanModalSlice";
function CommonPage() {
const dim = useSelector((state: RootState) => state.interface.dim);
const isAuthorized = useSelector((state: RootState) => state.auth.isAuthorized);
const isScanModalVisible = useSelector((state: RootState) => state.scanModal.isVisible);
const dispatch = useDispatch();
const loadSettings = async () => {
const token = await SecureStore.getItemAsync('accessToken');
if (!token) return;
dispatch(login({accessToken: token}));
}
useEffect(() => {
loadSettings();
}, []);
return (
<View style={styles.main}>
<MainScreen/>
{isAuthorized ? <MainScreen/> : <LoginScreen/>}
<View style={[styles.overlay, {display: dim ? 'flex' : 'none'}]}/>
<Toast config={toastConfig}/>
<ScanModal visible={isScanModalVisible}
onCancelButtonPress={() => {
dispatch(closeScanModal());
}}
onChanged={text => {
dispatch(setScannedData(text));
dispatch(closeScanModal());
}}/>
</View>
)

View File

@@ -3,27 +3,37 @@ import {StyleSheet, Text, View, ImageBackground, Linking} from 'react-native';
import TelegramAuthButton from "components/TelegramAuthButton/TelegramAuthButton";
import WebView from "react-native-webview";
import InputField from "./components/InputField";
import {useDispatch, useSelector} from "react-redux";
import {loadToken, loginUser} from "features/auth/authSlice";
import {AppDispatch, RootState, useAppDispatch} from "redux/store";
import * as SecureStore from 'expo-secure-store';
import {initializeUseSelector} from "react-redux/es/hooks/useSelector";
import HomeScreen from "../HomeScreen/HomeScreen";
import {useAppDispatch} from "redux/store";
import {RFPercentage, RFValue} from "react-native-responsive-fontsize";
import {responsiveWidth} from "react-native-responsive-dimensions";
import userApi from "../../api/userApi";
import {useDispatch} from "react-redux";
import {login} from "../../features/auth/authSlice";
import Toast from "react-native-toast-message";
import * as SecureStore from 'expo-secure-store';
function LoginScreen() {
const dispatch = useAppDispatch();
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const {status, errorMessage, isAuthenticated} = useSelector((state: RootState) => state.auth);
const [loginValue, setLoginValue] = useState('dsnon');
const [passwordValue, setPasswordValue] = useState('ochko');
const handleLogin = async () => {
dispatch(loginUser({login: login, password: password}));
const response = await userApi.login(loginValue, passwordValue);
if (!response.ok) {
Toast.show({
type: 'error',
text1: 'Ошибка авторизации',
text2: 'Неудалось войти используя указанные данные!'
});
setLoginValue("");
setPasswordValue("");
return;
}
await SecureStore.setItemAsync('accessToken', response.accessToken);
dispatch(login({accessToken: response.accessToken}));
}
useEffect(() => {
dispatch(loadToken());
}, []);
return (
<>
@@ -32,10 +42,9 @@ function LoginScreen() {
style={styles.image}>
<View style={styles.block}>
<Text style={styles.authText}>Авторизация</Text>
<InputField onChange={setLogin} placeholder={"Логин"}/>
<InputField onChange={setPassword} secureTextEntry={false} placeholder={"Пароль"}/>
<InputField value={loginValue} onChange={setLoginValue} placeholder={"Логин"}/>
<InputField value={passwordValue} onChange={setPasswordValue} secureTextEntry={false} placeholder={"Пароль"}/>
<TelegramAuthButton onPress={handleLogin}/>
<Text style={{fontSize: 36}}>{errorMessage}</Text>
</View>
</ImageBackground>
</View>}
@@ -64,7 +73,6 @@ const styles = StyleSheet.create({
},
authText: {
color: '#2478F8',
fontFamily: 'SF Pro Text',
fontSize: RFPercentage(3),
fontStyle: 'normal',
fontWeight: '500',

View File

@@ -7,13 +7,15 @@ type Props = {
placeholder?: string;
onChange?: (text: string) => void,
secureTextEntry?: boolean
value?: string
}
const InputField: FC<Props> = ({placeholder, onChange, secureTextEntry = false}) => {
const InputField: FC<Props> = ({placeholder, onChange, value, secureTextEntry = false}) => {
return (
<View style={styles.fieldContainer}>
<View style={styles.textInputWrapper}>
<TextInput
value={value}
placeholder={placeholder}
autoCorrect={false}
autoCapitalize={"none"}

View File

@@ -1,6 +1,6 @@
import {Button, Text, View} from "react-native";
import {useAppDispatch} from "../../redux/store";
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice.ts.back";
import * as process from "process";
function MoneyScreen() {

View File

@@ -6,6 +6,7 @@ import DTitle from "../../components/DTitle/DTitle";
import BasicButton from "../../components/BasicButton/BasicButton";
import Hyperlink from "../../components/Hyperlink/Hyperlink";
import React, {useState} from "react";
import userApi from "../../api/userApi";
type ArticleTextProps = {
article: number
@@ -48,8 +49,9 @@ function OrderScreen() {
<View style={styles.buttonsWrapper}>
{!order.assembled &&
<BasicButton label={"Отметить как собранный"} onPress={() => {
setOrder({...order, assembled: true})
console.log(order);
userApi.test()
// setOrder({...order, assembled: true})
// console.log(order);
}}/>
}

View File

@@ -1,100 +1,83 @@
import {ScrollView, StyleSheet, View} from "react-native";
import {ActivityIndicator, FlatList, ScrollView, StyleSheet, View} from "react-native";
import SearchBar from "../../components/SearchBar/SearchBar";
import OrderCard from "../../components/OrderCard/OrderCard";
import {RFPercentage} from "react-native-responsive-fontsize";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import SortingButton from "../../components/SortingButton/SortingButton";
import {useDispatch} from "react-redux";
import {useDispatch, useSelector} from "react-redux";
import {enableDim, disableDim} from "../../features/interface/interfaceSlice";
import {BottomSheetModal} from "@gorhom/bottom-sheet";
import {useMemo, useRef, useState} from "react";
import {useEffect, useMemo, useRef, useState} from "react";
import {RadioButton, Button} from "react-native-paper";
import DText from "../../components/DText/DText";
import {blue} from "../../css/colors";
import {background, blue} from "../../css/colors";
import DTitle from "../../components/DTitle/DTitle";
import BasicButton from "../../components/BasicButton/BasicButton";
import authSlice from "../../features/auth/authSlice";
import {generateRandomOrders, Order} from "../../types/order";
import {FlashList} from "@shopify/flash-list";
import SortingModal, {
SortingModalElement,
SortingModalHandles
} from "../../components/Modals/SortingModal/SortingModal";
import sortingModal from "../../components/Modals/SortingModal/SortingModal";
import flashListSeparator from "../../components/FlashListSeparator/FlashListSeparator";
import {RootState} from "../../redux/store";
import ordersApi from "../../api/ordersApi";
function OrdersScreen() {
const dispatch = useDispatch();
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const snapPoints = useMemo(() => ['30%', '30%'], []);
const [sortingValue, setSortingValue] = useState("createdOn");
const [orders, setOrders] = useState<Order[]>([]);
const sortingModalRef = useRef<SortingModalHandles | null>(null);
const defaultSortingValue = 'createdOnAsc';
const sortingModalElements: SortingModalElement[] = [
{id: 'createdOnAsc', value: 'createdOnAsc', label: 'Дата создания по убыванию'},
{id: 'createdOnDesc', value: 'createdOnDesc', label: 'Дата создания по возрастанию'},
{id: 'shipmentDateAsc', value: 'shipmentDateAsc', label: 'Дата отгрузки по убыванию'},
{id: 'shipmentDateDesc', value: 'shipmentDateDesc', label: 'Дата отгрузки по возрастанию'},
];
const [sortingValue, setSortingValue] = useState(defaultSortingValue);
return (
<View style={styles.container}>
<SearchBar onSearch={() => {
}} onSupplierProductSelected={product => {
ordersApi.getOrdersBySupplierProduct(product.supplierProductId).then(setOrders)
}}/>
<View style={styles.sortingButtonWrapper}>
<SortingButton onPress={() => {
dispatch(enableDim());
bottomSheetModalRef.current?.present();
if (!sortingModalRef.current) return;
sortingModalRef.current.present();
}}/>
</View>
<View style={styles.content}>
<ScrollView keyboardShouldPersistTaps={"never"} showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContainer}>
<FlashList
keyboardShouldPersistTaps={"never"}
data={orders}
keyExtractor={(item: Order) => item.orderNumber.toString()}
renderItem={({item}) =>
<OrderCard order={item} onPress={() => {
<OrderCard/>
<OrderCard/>
<OrderCard/>
<OrderCard/>
<OrderCard/>
<OrderCard/>
<OrderCard/>
</ScrollView>
}}/>}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={true}
onEndReachedThreshold={0.1}
estimatedItemSize={720}
onEndReached={() => {
}}
ItemSeparatorComponent={flashListSeparator}
/>
<SortingModal onChange={setSortingValue}
onClose={() => {
console.log("Closed")
}}
ref={sortingModalRef}
elements={sortingModalElements}
defaultElementId={defaultSortingValue}
/>
</View>
<BottomSheetModal
ref={bottomSheetModalRef}
snapPoints={snapPoints}
onDismiss={() => {
dispatch(disableDim());
}}>
<View style={styles.sortingModalContainer}>
<View style={styles.radioButtonContainer}>
<RadioButton
color={blue}
uncheckedColor={'black'}
value={'createdOn'}
status={sortingValue == 'createdOn' ? 'checked' : 'unchecked'}
onPress={() => setSortingValue('createdOn')}
/>
<DText>По дате создания</DText>
</View>
<View style={styles.radioButtonContainer}>
<RadioButton
color={blue}
uncheckedColor={'black'}
value={'shipmentDate'}
status={sortingValue == 'shipmentDate' ? 'checked' : 'unchecked'}
onPress={() => setSortingValue('shipmentDate')}
/>
<DText>По дате отгрузки</DText>
</View>
<View style={styles.radioButtonContainer}>
<RadioButton
color={blue}
uncheckedColor={'black'}
value={'shipmentDatee'}
status={sortingValue == 'shipmentDatee' ? 'checked' : 'unchecked'}
onPress={() => setSortingValue('shipmentDatee')}
/>
<DText>По дате дрочки</DText>
</View>
<View style={styles.sortingModalButton}>
<BasicButton onPress={() => {
bottomSheetModalRef.current?.dismiss();
}} label={'Применть'}/>
</View>
</View>
</BottomSheetModal>
</View>
)
}
@@ -112,8 +95,7 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContainer: {
rowGap: responsiveHeight(2)
rowGap: responsiveHeight(2),
},
radioButtonContainer: {
flexDirection: "row",

View File

@@ -1,4 +1,4 @@
import {Button, Text, View, StyleSheet, TouchableOpacity, Image, ScrollView} from "react-native";
import {Button, Text, View, StyleSheet, TouchableOpacity, Image, ScrollView, GestureResponderEvent} from "react-native";
import {useAppDispatch} from "../../redux/store";
import * as process from "process";
import {responsiveFontSize, responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
@@ -10,14 +10,19 @@ import {ScreenStackHeaderLeftView} from "react-native-screens";
import Separator from "../../components/Separator/Separator";
import {BottomSheetModal} from "@gorhom/bottom-sheet";
import {useMemo, useRef, useState} from "react";
import {openApp} from "rn-openapp";
import SelectProductModal from "../../components/Modals/SelectProductModal/SelectProductModal";
import selectProductModal from "../../components/Modals/SelectProductModal/SelectProductModal";
type SettingsElementProps = {
icon: any;
title: string;
onPress?: (event: GestureResponderEvent) => void
}
const SettingsElement: React.FC<SettingsElementProps> = ({icon, title}) => {
const SettingsElement: React.FC<SettingsElementProps> = ({icon, title, onPress}) => {
return (
<TouchableOpacity>
<TouchableOpacity onPress={onPress}>
<View style={styles.actionsCarouselElementContainer}>
<View style={styles.actionsCarouselImageWrapper}>
@@ -49,6 +54,7 @@ const HistoryElement: React.FC<HistoryElementProps> = ({cost, description, color
}
function ProfileScreen() {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const snapPoints = useMemo(() => ['25%', '40%'], []);
const [modalVisible, setModalVisible] = useState(false);
@@ -70,7 +76,9 @@ function ProfileScreen() {
>
<SettingsElement icon={require('assets/icons/settings/withdraw.png')} title={'Вывод'}/>
<SettingsElement icon={require('assets/icons/settings/statistics.png')} title={'Статистика'}/>
<SettingsElement icon={require('assets/icons/settings/printer.png')} title={'Принтеры'}/>
<SettingsElement onPress={() => {
openApp('assemblrprintingservice');
}} icon={require('assets/icons/settings/printer.png')} title={'Принтеры'}/>
</ScrollView>
</View>
<Separator/>
@@ -107,6 +115,7 @@ function ProfileScreen() {
</BottomSheetModal>
<View style={[styles.overlay, {display: modalVisible ? 'flex' : 'none'}]}/>
</View>
)
}

View File

@@ -1,4 +1,45 @@
export type Order = {
id: number;
databaseId: number;
article: number;
imageUrl: string;
orderNumber: string;
productName: string;
supplierName: string;
marketplaceName: string;
sellerName: string;
assembled: boolean;
};
// Генератор случайных строк заданной длины
function generateRandomString(length: number): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
// Генератор случайных данных для заказа
function generateRandomOrder(): Order {
return {
databaseId: Math.floor(Math.random() * 1000), // Произвольное число для databaseId
article: Math.floor(Math.random() * 100000), // Произвольное число для article
imageUrl: 'https://example.com/image.png', // Произвольная ссылка на изображение
orderNumber: generateRandomString(12), // Случайная строка длиной 12 символов
productName: 'Random Product', // Произвольное название продукта
supplierName: 'Random Supplier', // Произвольное имя поставщика
marketplaceName: 'Random Marketplace', // Произвольное имя маркетплейса
sellerName: 'Random Seller', // Произвольное имя продавца
assembled: Math.random() < 0.5, // Произвольное булевое значение (true/false)
};
}
// Генератор массива случайных заказов
export function generateRandomOrders(count: number): Order[] {
const orders: Order[] = [];
for (let i = 0; i < count; i++) {
orders.push(generateRandomOrder());
}
return orders;
}

View File

@@ -0,0 +1,11 @@
export type SupplierProduct = {
dencoArticle: number;
productId: number;
supplierProductId: number;
productName: string;
supplierName: string;
supplierArticle: string;
inBlock: number;
shelfNumber: string;
thumbUrl: string;
};