Compare commits
2 Commits
7bca25a30c
...
a6531ca88a
| Author | SHA1 | Date | |
|---|---|---|---|
| a6531ca88a | |||
| 9010574b59 |
2
app.json
2
app.json
@@ -15,7 +15,7 @@
|
||||
],
|
||||
"name": "Assemblr",
|
||||
"slug": "Assemblr",
|
||||
"version": "1.1.7",
|
||||
"version": "1.3.4",
|
||||
"orientation": "portrait",
|
||||
"icon": "./src/assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.6",
|
||||
"react-native-animatable": "^1.4.0",
|
||||
"react-native-gesture-handler": "~2.12.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-keyevent": "^0.3.1",
|
||||
@@ -58,7 +59,8 @@
|
||||
"react-native-webview": "13.2.2",
|
||||
"react-redux": "^8.1.2",
|
||||
"redux": "^4.2.1",
|
||||
"rn-openapp": "^2.1.2"
|
||||
"rn-openapp": "^2.1.2",
|
||||
"expo-av": "~13.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
||||
12
src/App.tsx
12
src/App.tsx
@@ -1,28 +1,18 @@
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Provider} from "react-redux";
|
||||
|
||||
import {useFonts} from 'expo-font';
|
||||
import {store} from "./redux/store";
|
||||
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";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
export default function App() {
|
||||
let [fontsLoading] = useFonts({
|
||||
// 'SF Pro Text': require('./assets/fonts/SF-Pro-Text-Regular.otf')
|
||||
})
|
||||
|
||||
if (!fontsLoading)
|
||||
return <View><Text>Loading...</Text></View>
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<GestureHandlerRootView style={{flex: 1}}>
|
||||
<BottomSheetModalProvider>
|
||||
<CommonPage/>
|
||||
|
||||
</BottomSheetModalProvider>
|
||||
</GestureHandlerRootView>
|
||||
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
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";
|
||||
import axios from 'axios';
|
||||
|
||||
export const baseUrl = 'https://assemblr.denco.store';
|
||||
// export const baseUrl = 'http://192.168.1.101:5000';
|
||||
const apiClient = axios.create({
|
||||
baseURL: baseUrl
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
const accessToken = await SecureStore.getItemAsync('accessToken');
|
||||
if (!config.headers) {
|
||||
config.headers = new AxiosHeaders();
|
||||
}
|
||||
if (accessToken) {
|
||||
config.headers.set('Authorization', `Bearer ${accessToken}`, true);
|
||||
}
|
||||
config.validateStatus = (status) => {
|
||||
if (status == 401) {
|
||||
SecureStore.deleteItemAsync('accessToken');
|
||||
store.dispatch(logout());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return config;
|
||||
}, function (error) {
|
||||
});
|
||||
|
||||
|
||||
export default apiClient;
|
||||
|
||||
@@ -15,7 +15,7 @@ const assemblyApi = {
|
||||
let response = await apiClient.post(`${router}/create`, {orderId});
|
||||
return response.data;
|
||||
},
|
||||
close: async (assemblyId: number): Promise<{ ok: boolean, message: string }> => {
|
||||
close: async (assemblyId: number): Promise<{ ok: boolean, message: string, reward: number }> => {
|
||||
let response = await apiClient.post(`${router}/close`, {assemblyId});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
16
src/api/balanceApi.ts
Normal file
16
src/api/balanceApi.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {BalanceInfo, BalanceTransaction} from "../types/balance";
|
||||
import apiClient from "./apiClient";
|
||||
|
||||
const router = '/balance';
|
||||
const balanceApi = {
|
||||
getTransactions: async (page: number): Promise<{ balanceTransactions: BalanceTransaction[] }> => {
|
||||
const response = await apiClient.get(`${router}/transactions`, {params: {page}});
|
||||
return response.data;
|
||||
},
|
||||
getBalanceInfo: async (): Promise<BalanceInfo> => {
|
||||
const response = await apiClient.get(`${router}/info`);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export default balanceApi;
|
||||
BIN
src/assets/icons/reward.png
Normal file
BIN
src/assets/icons/reward.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
93
src/components/Animations/AnimationsView.tsx
Normal file
93
src/components/Animations/AnimationsView.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {StyleSheet, View, Image} from "react-native";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import DText from "../DText/DText";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState, useAppDispatch} from "../../redux/store";
|
||||
import {hideReward} from "../../features/animations/animationsSlice";
|
||||
|
||||
// const width = responsiveWidth(20)
|
||||
const AnimationsView: FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const state = useSelector((state: RootState) => state.animations);
|
||||
const [currentAnimation, setCurrentAnimation] = useState('bounceInRight');
|
||||
const [animationStage, setAnimationStage] = useState('in'); // Управляет текущей стадией анимации
|
||||
|
||||
useEffect(() => {
|
||||
// После завершения анимации входа, начинаем анимацию выхода
|
||||
if (animationStage === 'out') {
|
||||
setCurrentAnimation('bounceOutRight');
|
||||
}
|
||||
}, [animationStage]);
|
||||
|
||||
const onAnimationEnd = () => {
|
||||
if (animationStage === 'in') {
|
||||
// После завершения анимации входа, переходим к анимации выхода
|
||||
setAnimationStage('out');
|
||||
} else if (animationStage === "out") {
|
||||
dispatch(hideReward());
|
||||
}
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
setCurrentAnimation("bounceInRight");
|
||||
setAnimationStage('in');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isRewardAnimationVisible)
|
||||
startAnimation();
|
||||
|
||||
}, [state.isRewardAnimationVisible]);
|
||||
|
||||
return (
|
||||
<View style={{...style.container, display: state.isRewardAnimationVisible ? "flex" : "none"}}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: responsiveWidth(3),
|
||||
// width: width,
|
||||
paddingHorizontal: responsiveWidth(2),
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: responsiveWidth(1),
|
||||
zIndex: 1,
|
||||
elevation: 10,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
animation={currentAnimation}
|
||||
duration={1000} // Продолжительность анимации
|
||||
easing="ease-in-out" // Тип анимации
|
||||
iterationCount={1} // Бесконечное повторение
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
>
|
||||
<View style={{
|
||||
height: responsiveHeight(5)
|
||||
}}>
|
||||
<Image
|
||||
style={{
|
||||
height: responsiveHeight(5),
|
||||
width:responsiveWidth(5),
|
||||
resizeMode: "contain"
|
||||
}}
|
||||
source={require('assets/icons/reward.png')}/>
|
||||
</View>
|
||||
<DText>+{state.rewardAmount.toLocaleString('ru-RU')}₽</DText>
|
||||
|
||||
</Animatable.View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
marginTop: responsiveHeight(10),
|
||||
|
||||
}
|
||||
})
|
||||
export default AnimationsView;
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, {FC, ReactElement} from "react";
|
||||
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import React, {FC, ReactNode} from "react";
|
||||
import {StyleProp, StyleSheet, TextStyle} from 'react-native';
|
||||
import DText from "../DText/DText";
|
||||
import {responsiveScreenFontSize, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {responsiveScreenFontSize} from "react-native-responsive-dimensions";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
children: ReactNode;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
const DTitle: FC<Props> = ({children, style}) => {
|
||||
|
||||
25
src/contexts/apiContext.tsx
Normal file
25
src/contexts/apiContext.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import {createContext, FC, ReactNode, useContext} from "react";
|
||||
|
||||
type ApiState = {
|
||||
onLogout: () => void;
|
||||
};
|
||||
const apiContext = createContext<ApiState | undefined>(undefined);
|
||||
|
||||
export const useApiContext = () => {
|
||||
const context = useContext(apiContext);
|
||||
if (!context) throw new Error('useApiContext must be used within ApiContextProvider');
|
||||
return context;
|
||||
}
|
||||
|
||||
type ApiContextProviderProps = {
|
||||
children: ReactNode;
|
||||
state: ApiState;
|
||||
}
|
||||
export const ApiContextProvider: FC<ApiContextProviderProps> = ({children, state}) => {
|
||||
|
||||
return (
|
||||
<ApiContextProvider state={state}>
|
||||
{children}
|
||||
</ApiContextProvider>
|
||||
)
|
||||
}
|
||||
29
src/features/animations/animationsSlice.ts
Normal file
29
src/features/animations/animationsSlice.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
|
||||
interface AnimationsSlice {
|
||||
isRewardAnimationVisible: boolean;
|
||||
rewardAmount: number;
|
||||
}
|
||||
|
||||
const initialState: AnimationsSlice = {
|
||||
isRewardAnimationVisible: false,
|
||||
rewardAmount: 0
|
||||
}
|
||||
|
||||
export const animationsSlice = createSlice({
|
||||
name: 'animations',
|
||||
initialState,
|
||||
reducers: {
|
||||
showReward: (state, action: PayloadAction<{ reward: number }>) => {
|
||||
state.rewardAmount = action.payload.reward;
|
||||
state.isRewardAnimationVisible = true;
|
||||
},
|
||||
hideReward: (state) => {
|
||||
state.isRewardAnimationVisible = false;
|
||||
state.rewardAmount = 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const {showReward, hideReward} = animationsSlice.actions;
|
||||
export default animationsSlice.reducer;
|
||||
71
src/features/balance/balanceSlice.ts
Normal file
71
src/features/balance/balanceSlice.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {BalanceInfo, BalanceTransaction} from "../../types/balance";
|
||||
import {createAsyncThunk, createSlice} from "@reduxjs/toolkit";
|
||||
import balanceApi from "../../api/balanceApi";
|
||||
|
||||
const name = 'balance';
|
||||
|
||||
interface TransactionsState {
|
||||
items: BalanceTransaction[];
|
||||
isLoading: boolean;
|
||||
currentPage: number;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
interface BalanceState {
|
||||
balance: number;
|
||||
transactions: TransactionsState;
|
||||
}
|
||||
|
||||
const transactionsInitialState: TransactionsState = {
|
||||
currentPage: 1,
|
||||
hasNext: true,
|
||||
isLoading: false,
|
||||
items: []
|
||||
}
|
||||
const initialState: BalanceState = {
|
||||
balance: 0,
|
||||
transactions: transactionsInitialState
|
||||
}
|
||||
|
||||
|
||||
export const fetchTransactions = createAsyncThunk(
|
||||
`${name}/fetchTransactions`,
|
||||
async (page: number, _): Promise<BalanceTransaction[]> => {
|
||||
const response = await balanceApi.getTransactions(page);
|
||||
return response.balanceTransactions;
|
||||
}
|
||||
)
|
||||
|
||||
export const fetchBalance = createAsyncThunk(
|
||||
`${name}/fetchBalance`,
|
||||
async (_): Promise<BalanceInfo> => {
|
||||
return await balanceApi.getBalanceInfo();
|
||||
}
|
||||
)
|
||||
export const balanceSlice = createSlice({
|
||||
name: name,
|
||||
initialState,
|
||||
reducers: {
|
||||
refreshTransactions: (state) => {
|
||||
state.transactions = transactionsInitialState;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(fetchTransactions.pending, (state, action) => {
|
||||
state.transactions.isLoading = true;
|
||||
})
|
||||
builder.addCase(fetchTransactions.fulfilled, (state, action) => {
|
||||
state.transactions.isLoading = false;
|
||||
state.transactions.hasNext = action.payload.length > 0;
|
||||
state.transactions.items = [...state.transactions.items, ...action.payload];
|
||||
state.transactions.currentPage = state.transactions.currentPage + 1;
|
||||
})
|
||||
builder.addCase(fetchBalance.fulfilled, (state, action) => {
|
||||
state.balance = action.payload.balance;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const {refreshTransactions} = balanceSlice.actions;
|
||||
|
||||
export default balanceSlice.reducer;
|
||||
33
src/redux/general.ts
Normal file
33
src/redux/general.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {AxiosError, AxiosRequestConfig} from "axios";
|
||||
import {BaseQueryFn} from "@reduxjs/toolkit/query";
|
||||
import apiClient from "../api/apiClient";
|
||||
|
||||
export const axiosBaseQuery =
|
||||
(): BaseQueryFn<
|
||||
{
|
||||
url: string
|
||||
method?: AxiosRequestConfig['method']
|
||||
data?: AxiosRequestConfig['data']
|
||||
params?: AxiosRequestConfig['params']
|
||||
headers?: AxiosRequestConfig['headers']
|
||||
}
|
||||
> =>
|
||||
async ({method, data, params, headers}) => {
|
||||
try {
|
||||
const result = await apiClient({
|
||||
method,
|
||||
data,
|
||||
params,
|
||||
headers,
|
||||
})
|
||||
return {data: result.data}
|
||||
} catch (axiosError) {
|
||||
const err = axiosError as AxiosError
|
||||
return {
|
||||
error: {
|
||||
status: err.response?.status,
|
||||
data: err.response?.data || err.message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import ordersFilterReducer from 'features/ordersFilter/ordersFilterSlice';
|
||||
import shippingWarehouseSelectReducer from 'features/shippingWarehouseSelect/shippingWarehouseSelectSlice';
|
||||
import citySelectReducer from 'features/citySelect/citySelectSlice';
|
||||
import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModalSlice';
|
||||
import balanceReducer from 'features/balance/balanceSlice';
|
||||
import animationsReducer from 'features/animations/animationsSlice';
|
||||
import {useDispatch} from "react-redux";
|
||||
|
||||
export const store = configureStore({
|
||||
@@ -27,7 +29,9 @@ export const store = configureStore({
|
||||
ordersFilter: ordersFilterReducer,
|
||||
shippingWarehouseSelect: shippingWarehouseSelectReducer,
|
||||
citySelect: citySelectReducer,
|
||||
cancelAssemblyModal: cancelAssemblyModal
|
||||
cancelAssemblyModal: cancelAssemblyModal,
|
||||
balance: balanceReducer,
|
||||
animations: animationsReducer
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {useEffect} from "react";
|
||||
import {background} from "../../css/colors";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store";
|
||||
import {login} from "../../features/auth/authSlice";
|
||||
import {login, logout} from "../../features/auth/authSlice";
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import Toast from "react-native-toast-message";
|
||||
import toastConfig from "../../components/Toast/Toast";
|
||||
@@ -21,9 +21,7 @@ import {ASSEMBLY_STATE} from "../../types/assembly";
|
||||
import Constants from "expo-constants";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import applicationApi from "../../api/applicationApi";
|
||||
import {ActivityAction, startActivityAsync} from "expo-intent-launcher";
|
||||
import {RenderTargetOptions} from "@shopify/flash-list";
|
||||
import KeyEvent from 'react-native-keyevent';
|
||||
import {startActivityAsync} from "expo-intent-launcher";
|
||||
import {
|
||||
openLoadingModal,
|
||||
setIndeterminate,
|
||||
@@ -31,8 +29,12 @@ import {
|
||||
setProgress
|
||||
} from "../../features/loadingModal/loadingModalSlice";
|
||||
import CancelAssemblyModal from "../../components/Modals/CancelAssemblyModal/CancelAssemblyModal";
|
||||
import {AxiosHeaders} from "axios";
|
||||
import apiClient from "../../api/apiClient";
|
||||
import AnimationsView from "../../components/Animations/AnimationsView";
|
||||
|
||||
function CommonPage() {
|
||||
|
||||
const dim = useSelector((state: RootState) => state.interface.dim);
|
||||
const isAuthorized = useSelector((state: RootState) => state.auth.isAuthorized);
|
||||
|
||||
@@ -85,13 +87,42 @@ function CommonPage() {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const configApi = () => {
|
||||
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
const accessToken = await SecureStore.getItemAsync('accessToken');
|
||||
if (!config.headers) {
|
||||
config.headers = new AxiosHeaders();
|
||||
}
|
||||
if (accessToken) {
|
||||
config.headers.set('Authorization', `Bearer ${accessToken}`, true);
|
||||
}
|
||||
config.validateStatus = (status) => {
|
||||
if (status == 401) {
|
||||
SecureStore.deleteItemAsync('accessToken');
|
||||
dispatch(logout());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return config;
|
||||
}, function (error) {
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
configApi();
|
||||
checkUpdates();
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
<View style={styles.main}>
|
||||
{isAuthorized ? <MainScreen/> : <LoginScreen/>}
|
||||
<AnimationsView/>
|
||||
<View style={[styles.overlay, {display: dim ? 'flex' : 'none'}]}/>
|
||||
<LoadingModal/>
|
||||
<ScanModal/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {StyleSheet, Text, View, ImageBackground, Image} from 'react-native';
|
||||
import {StyleSheet, Image} from 'react-native';
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import BarcodeScreen from "../BarcodeScreen/BarcodeScreen";
|
||||
import {DefaultTheme, NavigationContainer} from "@react-navigation/native";
|
||||
@@ -6,10 +6,10 @@ import moneyScreen from "../MoneyScreen/MoneyScreen";
|
||||
import profileScreen from "../ProfileScreen/ProfileScreen";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import OrderScreen, {OrderScreenController} from "../OrderScreen/OrderScreen";
|
||||
import {OrderScreenController} from "../OrderScreen/OrderScreen";
|
||||
import OrdersScreen from "../OrdersScreen/OrdersScreen";
|
||||
import {background} from "../../css/colors";
|
||||
import * as fs from "fs";
|
||||
|
||||
|
||||
export type TabNavigatorParamList = {
|
||||
Home: undefined;
|
||||
@@ -30,16 +30,16 @@ const CustomTab = ({name, component, icon, hidden}: CustomTabProps) => ({
|
||||
name,
|
||||
component,
|
||||
options: {
|
||||
tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => (
|
||||
<Image
|
||||
tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => {
|
||||
return (<Image
|
||||
source={icon}
|
||||
style={{
|
||||
width: RFPercentage(4),
|
||||
height: RFPercentage(4),
|
||||
tintColor: color,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
/>)
|
||||
},
|
||||
tabBarLabel: () => null,
|
||||
}
|
||||
});
|
||||
@@ -90,6 +90,7 @@ function MainScreen() {
|
||||
},
|
||||
}}>
|
||||
|
||||
|
||||
<Tab.Navigator screenOptions={{
|
||||
tabBarStyle: styles.tabBarStyle,
|
||||
headerTitle: "",
|
||||
|
||||
@@ -5,8 +5,8 @@ import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import DTitle from "../../components/DTitle/DTitle";
|
||||
import BasicButton from "../../components/BasicButton/BasicButton";
|
||||
import React, {FC, useEffect, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState, useAppDispatch} from "../../redux/store";
|
||||
import {closeLoadingModal, openLoadingModal, setLoadingText} from "../../features/loadingModal/loadingModalSlice";
|
||||
import {NavigationProp, useNavigation} from "@react-navigation/native";
|
||||
import printingApi from "../../api/printingApi";
|
||||
@@ -31,6 +31,8 @@ import {TabNavigatorParamList} from "../MainScreen/MainScreen";
|
||||
import {openImageZoomModal, setImages} from "../../features/imageZoomModal/loadingModalSlice";
|
||||
import {OrderStatus, OrderStatusDictionary} from "../../features/ordersFilter/ordersFilterSlice";
|
||||
import {openCancelAssemblyModal} from "../../features/cancelAssemblyModal/cancelAssemblyModalSlice";
|
||||
import {fetchBalance, refreshTransactions} from "../../features/balance/balanceSlice";
|
||||
import {showReward} from "../../features/animations/animationsSlice";
|
||||
|
||||
|
||||
type AssemblyPeriod = {
|
||||
@@ -68,7 +70,7 @@ type OrderScreenProps = {
|
||||
const OrderScreen: FC<OrderScreenProps> = ({order}) => {
|
||||
const navigator = useNavigation<NavigationProp<TabNavigatorParamList, 'Barcode'>>();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const assembly = useSelector((state: RootState) => state.assembly.assembly);
|
||||
const assemblyState = useSelector((state: RootState) => state.assembly.localState);
|
||||
const initialOrder = useSelector((state: RootState) => state.assembly.initialOrder);
|
||||
@@ -144,12 +146,19 @@ const OrderScreen: FC<OrderScreenProps> = ({order}) => {
|
||||
label={"Завершить сборку"}
|
||||
onPress={() => {
|
||||
if (!assembly) return;
|
||||
assemblyApi.close(assembly.databaseId).then(({ok, message}) => {
|
||||
assemblyApi.close(assembly.databaseId).then(({ok, message, reward}) => {
|
||||
Toast.show({
|
||||
type: ok ? 'success' : 'error',
|
||||
text1: 'Завершение сборки',
|
||||
text2: message
|
||||
});
|
||||
if (ok) {
|
||||
dispatch(showReward({reward}));
|
||||
dispatch(fetchBalance())
|
||||
dispatch(refreshTransactions())
|
||||
|
||||
}
|
||||
|
||||
dispatch(endAssembly());
|
||||
navigator.navigate('Barcode');
|
||||
})
|
||||
|
||||
@@ -1,130 +1,54 @@
|
||||
import {View, StyleSheet, TouchableOpacity, Image, ScrollView, GestureResponderEvent} from "react-native";
|
||||
import {responsiveFontSize, responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {blue, gray} from "../../css/colors";
|
||||
import {View, StyleSheet} from "react-native";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {blue} from "../../css/colors";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import DText from "../../components/DText/DText";
|
||||
import DTitle from "../../components/DTitle/DTitle";
|
||||
import Separator from "../../components/Separator/Separator";
|
||||
import {BottomSheetModal} from "@gorhom/bottom-sheet";
|
||||
import {useMemo, useRef, useState} from "react";
|
||||
import assemblyApi from "../../api/assemblyApi";
|
||||
import Toast from "react-native-toast-message";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {reset} from "../../features/assembly/assemblySlice";
|
||||
import {useEffect} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import SettingsView from "./SettingsView";
|
||||
import TransactionsView from "./TransactionView";
|
||||
import {RootState, useAppDispatch} from "../../redux/store";
|
||||
import {fetchBalance, fetchTransactions, refreshTransactions} from "../../features/balance/balanceSlice";
|
||||
import {formatBalanceNumber} from "../../utils/formatters";
|
||||
|
||||
|
||||
type SettingsElementProps = {
|
||||
icon: any;
|
||||
title: string;
|
||||
onPress?: (event: GestureResponderEvent) => void
|
||||
|
||||
}
|
||||
const SettingsElement: React.FC<SettingsElementProps> = ({icon, title, onPress}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
|
||||
<View style={styles.actionsCarouselElementContainer}>
|
||||
<View style={styles.actionsCarouselImageWrapper}>
|
||||
<Image style={styles.actionsCarouselImage} source={icon}/>
|
||||
</View>
|
||||
<DText>{title}</DText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
)
|
||||
}
|
||||
type HistoryElementProps = {
|
||||
cost: number;
|
||||
description: string;
|
||||
color: string;
|
||||
}
|
||||
const HistoryElement: React.FC<HistoryElementProps> = ({cost, description, color}) => {
|
||||
const formatNumber = (n: number): string => n >= 0 ? `+${n}` : `${n}`;
|
||||
return (
|
||||
<View style={styles.historyElementContainer}>
|
||||
<DText style={{
|
||||
fontSize: responsiveFontSize(2),
|
||||
color: color,
|
||||
fontWeight: "500"
|
||||
}}>{formatNumber(cost)} руб</DText>
|
||||
<DText style={{fontSize: responsiveFontSize(1.5), color: gray, fontWeight: "400"}}>{description}</DText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileScreen() {
|
||||
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const snapPoints = useMemo(() => ['25%', '40%'], []);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const balanceState = useSelector((state: RootState) => state.balance);
|
||||
const onTransactionsEndReached = () => {
|
||||
if (balanceState.transactions.isLoading || !balanceState.transactions.hasNext) return;
|
||||
dispatch(fetchTransactions(balanceState.transactions.currentPage));
|
||||
};
|
||||
const onTransactionsRefresh = () => {
|
||||
dispatch(refreshTransactions());
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch(fetchBalance());
|
||||
}, []);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerInfo}>
|
||||
<DTitle style={styles.headerText}>Ваш баланс: 228 руб</DTitle>
|
||||
<DTitle style={styles.headerText}>Собрано товаров: 1337 шт</DTitle>
|
||||
<DTitle style={styles.headerText}>Ваш
|
||||
баланс: {formatBalanceNumber(balanceState.balance, false)} руб</DTitle>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.actionsCarouselContainer}>
|
||||
<ScrollView horizontal={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.actionsCarousel}
|
||||
>
|
||||
<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={() => {
|
||||
assemblyApi.cancel().then(response => {
|
||||
|
||||
Toast.show({
|
||||
type: response.ok ? "success" : "error",
|
||||
text1: "Отмена сборки",
|
||||
text2: response.message,
|
||||
});
|
||||
dispatch(reset());
|
||||
})
|
||||
}} icon={require('assets/icons/settings/close.png')} title={'Отменить сборку'}/>
|
||||
</ScrollView>
|
||||
<SettingsView/>
|
||||
</View>
|
||||
<Separator/>
|
||||
<View style={styles.historyContainer}>
|
||||
<DTitle>История операций</DTitle>
|
||||
<ScrollView
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={styles.historyElements}
|
||||
contentContainerStyle={styles.historyElementsContainer}>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
<HistoryElement cost={25} description={"сбор товара №36243869"} color={"#2478F8"}/>
|
||||
</ScrollView>
|
||||
<TransactionsView
|
||||
onRefresh={onTransactionsRefresh}
|
||||
isLoading={balanceState.transactions.isLoading}
|
||||
transactions={balanceState.transactions.items}
|
||||
onEndReached={onTransactionsEndReached}/>
|
||||
</View>
|
||||
</View>
|
||||
<BottomSheetModal ref={bottomSheetModalRef}
|
||||
snapPoints={snapPoints}
|
||||
onDismiss={() => {
|
||||
setModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
<DText>хуй</DText>
|
||||
</View>
|
||||
</BottomSheetModal>
|
||||
|
||||
<View style={[styles.overlay, {display: modalVisible ? 'flex' : 'none'}]}/>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -151,7 +75,8 @@ const styles = StyleSheet.create({
|
||||
historyContainer: {
|
||||
paddingVertical: responsiveHeight(2),
|
||||
flex: 1,
|
||||
// backgroundColor: "red"
|
||||
gap: responsiveHeight(1),
|
||||
marginBottom: responsiveHeight(6)
|
||||
},
|
||||
historyElements: {
|
||||
marginTop: responsiveHeight(3),
|
||||
|
||||
87
src/screens/ProfileScreen/SettingsView.tsx
Normal file
87
src/screens/ProfileScreen/SettingsView.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import {GestureResponderEvent, Image, ScrollView, StyleSheet, TouchableOpacity, View} from "react-native";
|
||||
import DText from "../../components/DText/DText";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import {FC} from "react";
|
||||
import assemblyApi from "../../api/assemblyApi";
|
||||
import Toast from "react-native-toast-message";
|
||||
import {reset} from "../../features/assembly/assemblySlice";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {responsiveWidth} from "react-native-responsive-dimensions";
|
||||
|
||||
type SettingsElementProps = {
|
||||
icon: any;
|
||||
title: string;
|
||||
onPress?: (event: GestureResponderEvent) => void
|
||||
|
||||
}
|
||||
const SettingsElement: FC<SettingsElementProps> = ({icon, title, onPress}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
|
||||
<View style={styles.actionsCarouselElementContainer}>
|
||||
<View style={styles.actionsCarouselImageWrapper}>
|
||||
<Image style={styles.actionsCarouselImage} source={icon}/>
|
||||
</View>
|
||||
<DText style={{textAlign: "center"}}>{title}</DText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsView: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<ScrollView horizontal={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.actionsCarousel}
|
||||
>
|
||||
<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={() => {
|
||||
assemblyApi.cancel().then(response => {
|
||||
|
||||
Toast.show({
|
||||
type: response.ok ? "success" : "error",
|
||||
text1: "Отмена сборки",
|
||||
text2: response.message,
|
||||
});
|
||||
dispatch(reset());
|
||||
})
|
||||
}} icon={require('assets/icons/settings/close.png')} title={'Отменить сборку'}/>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionsCarouselElementContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
maxWidth: RFPercentage(10),
|
||||
},
|
||||
actionsCarouselImageWrapper: {
|
||||
width: RFPercentage(10),
|
||||
height: RFPercentage(10),
|
||||
|
||||
backgroundColor: "white",
|
||||
borderRadius: 100,
|
||||
borderWidth: RFPercentage(0.3),
|
||||
padding: RFPercentage(1.5),
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
actionsCarouselImage: {
|
||||
resizeMode: "contain",
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
actionsCarousel: {
|
||||
flexDirection: "row",
|
||||
columnGap: responsiveWidth(3),
|
||||
},
|
||||
});
|
||||
|
||||
export default SettingsView;
|
||||
74
src/screens/ProfileScreen/TransactionView.tsx
Normal file
74
src/screens/ProfileScreen/TransactionView.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {ScrollView, StyleSheet, View} from "react-native";
|
||||
import DText from "../../components/DText/DText";
|
||||
import {responsiveFontSize, responsiveHeight} from "react-native-responsive-dimensions";
|
||||
import {gray} from "../../css/colors";
|
||||
import {BalanceTransaction} from "../../types/balance";
|
||||
import {FC} from "react";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import flashListSeparator from "../../components/FlashListSeparator/FlashListSeparator";
|
||||
import {formatBalanceNumber} from "../../utils/formatters";
|
||||
|
||||
type TransactionElementProps = {
|
||||
transaction: BalanceTransaction;
|
||||
}
|
||||
const TransactionElement: FC<TransactionElementProps> = ({transaction}) => {
|
||||
|
||||
return (
|
||||
<View style={styles.historyElementContainer}>
|
||||
<DText style={{
|
||||
fontSize: responsiveFontSize(2),
|
||||
fontWeight: "500"
|
||||
}}>{formatBalanceNumber(transaction.amount)} руб</DText>
|
||||
<DText style={{
|
||||
fontSize: responsiveFontSize(1.5),
|
||||
color: gray,
|
||||
fontWeight: "400"
|
||||
}}>{transaction.description}</DText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
type TransactionsViewProps = {
|
||||
transactions: BalanceTransaction[];
|
||||
onEndReached: () => void;
|
||||
onRefresh: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
const TransactionsView: FC<TransactionsViewProps> = ({transactions, isLoading, onEndReached, onRefresh}) => {
|
||||
return (
|
||||
<FlashList
|
||||
onRefresh={onRefresh}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ItemSeparatorComponent={flashListSeparator}
|
||||
data={transactions}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
keyboardShouldPersistTaps={"never"}
|
||||
onEndReachedThreshold={0.5}
|
||||
estimatedItemSize={100}
|
||||
renderItem={item =>
|
||||
<TransactionElement
|
||||
transaction={item.item}
|
||||
/>}
|
||||
onEndReached={onEndReached}
|
||||
refreshing={isLoading}
|
||||
>
|
||||
</FlashList>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
historyElementContainer: {
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
borderRadius: RFPercentage(2),
|
||||
padding: RFPercentage(2),
|
||||
elevation: 1
|
||||
},
|
||||
historyElementsContainer: {
|
||||
rowGap: responsiveHeight(2),
|
||||
},
|
||||
})
|
||||
|
||||
export default TransactionsView;
|
||||
3
src/screens/ProfileScreen/useBalance.tsx
Normal file
3
src/screens/ProfileScreen/useBalance.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
const useBalance = () => {
|
||||
|
||||
}
|
||||
18
src/types/balance.ts
Normal file
18
src/types/balance.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export enum BalanceTransactionType {
|
||||
TOP_UP,
|
||||
WITHDRAW
|
||||
}
|
||||
|
||||
export type BalanceTransaction = {
|
||||
id: number;
|
||||
type: BalanceTransactionType;
|
||||
userId: number;
|
||||
amount: number;
|
||||
description: string;
|
||||
jsonData?: object;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type BalanceInfo = {
|
||||
balance: number;
|
||||
}
|
||||
8
src/utils/formatters.ts
Normal file
8
src/utils/formatters.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const formatBalanceNumber = (n: number, addSign = true) => {
|
||||
const roundNumber = (n: number, digits: number): number => parseInt(n.toFixed(digits))
|
||||
const formatNumber = (n: number): string => n
|
||||
>= 0 ? `${addSign ? '+' : ''}${roundNumber(n, 2).toLocaleString('ru-RU')}` :
|
||||
`${roundNumber(n, 2).toLocaleString('ru-RU')}`;
|
||||
|
||||
return formatNumber(n);
|
||||
}
|
||||
Reference in New Issue
Block a user