Compare commits
2 Commits
7bca25a30c
...
a6531ca88a
| Author | SHA1 | Date | |
|---|---|---|---|
| a6531ca88a | |||
| 9010574b59 |
2
app.json
2
app.json
@@ -15,7 +15,7 @@
|
|||||||
],
|
],
|
||||||
"name": "Assemblr",
|
"name": "Assemblr",
|
||||||
"slug": "Assemblr",
|
"slug": "Assemblr",
|
||||||
"version": "1.1.7",
|
"version": "1.3.4",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./src/assets/icon.png",
|
"icon": "./src/assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.72.6",
|
"react-native": "0.72.6",
|
||||||
|
"react-native-animatable": "^1.4.0",
|
||||||
"react-native-gesture-handler": "~2.12.0",
|
"react-native-gesture-handler": "~2.12.0",
|
||||||
"react-native-image-zoom-viewer": "^3.0.1",
|
"react-native-image-zoom-viewer": "^3.0.1",
|
||||||
"react-native-keyevent": "^0.3.1",
|
"react-native-keyevent": "^0.3.1",
|
||||||
@@ -58,7 +59,8 @@
|
|||||||
"react-native-webview": "13.2.2",
|
"react-native-webview": "13.2.2",
|
||||||
"react-redux": "^8.1.2",
|
"react-redux": "^8.1.2",
|
||||||
"redux": "^4.2.1",
|
"redux": "^4.2.1",
|
||||||
"rn-openapp": "^2.1.2"
|
"rn-openapp": "^2.1.2",
|
||||||
|
"expo-av": "~13.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@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 {Provider} from "react-redux";
|
||||||
|
|
||||||
import {useFonts} from 'expo-font';
|
|
||||||
import {store} from "./redux/store";
|
import {store} from "./redux/store";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CommonPage from "./screens/CommonPage/CommonPage";
|
import CommonPage from "./screens/CommonPage/CommonPage";
|
||||||
import {BottomSheetModalProvider} from "@gorhom/bottom-sheet";
|
import {BottomSheetModalProvider} from "@gorhom/bottom-sheet";
|
||||||
import {GestureHandlerRootView} from "react-native-gesture-handler";
|
import {GestureHandlerRootView} from "react-native-gesture-handler";
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
import Constants from "expo-constants";
|
|
||||||
|
|
||||||
export default function App() {
|
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 (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<GestureHandlerRootView style={{flex: 1}}>
|
<GestureHandlerRootView style={{flex: 1}}>
|
||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<CommonPage/>
|
<CommonPage/>
|
||||||
|
|
||||||
</BottomSheetModalProvider>
|
</BottomSheetModalProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,10 @@
|
|||||||
import axios, {AxiosHeaders, AxiosRequestConfig, AxiosRequestHeaders, InternalAxiosRequestConfig} from 'axios';
|
import axios 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";
|
|
||||||
export const baseUrl = 'https://assemblr.denco.store';
|
export const baseUrl = 'https://assemblr.denco.store';
|
||||||
// export const baseUrl = 'http://192.168.1.101:5000';
|
// export const baseUrl = 'http://192.168.1.101:5000';
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: baseUrl
|
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;
|
export default apiClient;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const assemblyApi = {
|
|||||||
let response = await apiClient.post(`${router}/create`, {orderId});
|
let response = await apiClient.post(`${router}/create`, {orderId});
|
||||||
return response.data;
|
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});
|
let response = await apiClient.post(`${router}/close`, {assemblyId});
|
||||||
return response.data;
|
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 React, {FC, ReactNode} from "react";
|
||||||
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
import {StyleProp, StyleSheet, TextStyle} from 'react-native';
|
||||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
|
||||||
import DText from "../DText/DText";
|
import DText from "../DText/DText";
|
||||||
import {responsiveScreenFontSize, responsiveWidth} from "react-native-responsive-dimensions";
|
import {responsiveScreenFontSize} from "react-native-responsive-dimensions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: string;
|
children: ReactNode;
|
||||||
style?: StyleProp<TextStyle>;
|
style?: StyleProp<TextStyle>;
|
||||||
}
|
}
|
||||||
const DTitle: FC<Props> = ({children, style}) => {
|
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 shippingWarehouseSelectReducer from 'features/shippingWarehouseSelect/shippingWarehouseSelectSlice';
|
||||||
import citySelectReducer from 'features/citySelect/citySelectSlice';
|
import citySelectReducer from 'features/citySelect/citySelectSlice';
|
||||||
import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModalSlice';
|
import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModalSlice';
|
||||||
|
import balanceReducer from 'features/balance/balanceSlice';
|
||||||
|
import animationsReducer from 'features/animations/animationsSlice';
|
||||||
import {useDispatch} from "react-redux";
|
import {useDispatch} from "react-redux";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
@@ -27,7 +29,9 @@ export const store = configureStore({
|
|||||||
ordersFilter: ordersFilterReducer,
|
ordersFilter: ordersFilterReducer,
|
||||||
shippingWarehouseSelect: shippingWarehouseSelectReducer,
|
shippingWarehouseSelect: shippingWarehouseSelectReducer,
|
||||||
citySelect: citySelectReducer,
|
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 {background} from "../../css/colors";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {RootState} from "../../redux/store";
|
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 * as SecureStore from 'expo-secure-store';
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import toastConfig from "../../components/Toast/Toast";
|
import toastConfig from "../../components/Toast/Toast";
|
||||||
@@ -21,9 +21,7 @@ import {ASSEMBLY_STATE} from "../../types/assembly";
|
|||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import applicationApi from "../../api/applicationApi";
|
import applicationApi from "../../api/applicationApi";
|
||||||
import {ActivityAction, startActivityAsync} from "expo-intent-launcher";
|
import {startActivityAsync} from "expo-intent-launcher";
|
||||||
import {RenderTargetOptions} from "@shopify/flash-list";
|
|
||||||
import KeyEvent from 'react-native-keyevent';
|
|
||||||
import {
|
import {
|
||||||
openLoadingModal,
|
openLoadingModal,
|
||||||
setIndeterminate,
|
setIndeterminate,
|
||||||
@@ -31,8 +29,12 @@ import {
|
|||||||
setProgress
|
setProgress
|
||||||
} from "../../features/loadingModal/loadingModalSlice";
|
} from "../../features/loadingModal/loadingModalSlice";
|
||||||
import CancelAssemblyModal from "../../components/Modals/CancelAssemblyModal/CancelAssemblyModal";
|
import CancelAssemblyModal from "../../components/Modals/CancelAssemblyModal/CancelAssemblyModal";
|
||||||
|
import {AxiosHeaders} from "axios";
|
||||||
|
import apiClient from "../../api/apiClient";
|
||||||
|
import AnimationsView from "../../components/Animations/AnimationsView";
|
||||||
|
|
||||||
function CommonPage() {
|
function CommonPage() {
|
||||||
|
|
||||||
const dim = useSelector((state: RootState) => state.interface.dim);
|
const dim = useSelector((state: RootState) => state.interface.dim);
|
||||||
const isAuthorized = useSelector((state: RootState) => state.auth.isAuthorized);
|
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(() => {
|
useEffect(() => {
|
||||||
|
configApi();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
|
loadSettings();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<View style={styles.main}>
|
<View style={styles.main}>
|
||||||
{isAuthorized ? <MainScreen/> : <LoginScreen/>}
|
{isAuthorized ? <MainScreen/> : <LoginScreen/>}
|
||||||
|
<AnimationsView/>
|
||||||
<View style={[styles.overlay, {display: dim ? 'flex' : 'none'}]}/>
|
<View style={[styles.overlay, {display: dim ? 'flex' : 'none'}]}/>
|
||||||
<LoadingModal/>
|
<LoadingModal/>
|
||||||
<ScanModal/>
|
<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 {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||||
import BarcodeScreen from "../BarcodeScreen/BarcodeScreen";
|
import BarcodeScreen from "../BarcodeScreen/BarcodeScreen";
|
||||||
import {DefaultTheme, NavigationContainer} from "@react-navigation/native";
|
import {DefaultTheme, NavigationContainer} from "@react-navigation/native";
|
||||||
@@ -6,10 +6,10 @@ import moneyScreen from "../MoneyScreen/MoneyScreen";
|
|||||||
import profileScreen from "../ProfileScreen/ProfileScreen";
|
import profileScreen from "../ProfileScreen/ProfileScreen";
|
||||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||||
import OrderScreen, {OrderScreenController} from "../OrderScreen/OrderScreen";
|
import {OrderScreenController} from "../OrderScreen/OrderScreen";
|
||||||
import OrdersScreen from "../OrdersScreen/OrdersScreen";
|
import OrdersScreen from "../OrdersScreen/OrdersScreen";
|
||||||
import {background} from "../../css/colors";
|
import {background} from "../../css/colors";
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export type TabNavigatorParamList = {
|
export type TabNavigatorParamList = {
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
@@ -30,16 +30,16 @@ const CustomTab = ({name, component, icon, hidden}: CustomTabProps) => ({
|
|||||||
name,
|
name,
|
||||||
component,
|
component,
|
||||||
options: {
|
options: {
|
||||||
tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => (
|
tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => {
|
||||||
<Image
|
return (<Image
|
||||||
source={icon}
|
source={icon}
|
||||||
style={{
|
style={{
|
||||||
width: RFPercentage(4),
|
width: RFPercentage(4),
|
||||||
height: RFPercentage(4),
|
height: RFPercentage(4),
|
||||||
tintColor: color,
|
tintColor: color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>)
|
||||||
),
|
},
|
||||||
tabBarLabel: () => null,
|
tabBarLabel: () => null,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,6 +90,7 @@ function MainScreen() {
|
|||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
|
|
||||||
<Tab.Navigator screenOptions={{
|
<Tab.Navigator screenOptions={{
|
||||||
tabBarStyle: styles.tabBarStyle,
|
tabBarStyle: styles.tabBarStyle,
|
||||||
headerTitle: "",
|
headerTitle: "",
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {RFPercentage} from "react-native-responsive-fontsize";
|
|||||||
import DTitle from "../../components/DTitle/DTitle";
|
import DTitle from "../../components/DTitle/DTitle";
|
||||||
import BasicButton from "../../components/BasicButton/BasicButton";
|
import BasicButton from "../../components/BasicButton/BasicButton";
|
||||||
import React, {FC, useEffect, useState} from "react";
|
import React, {FC, useEffect, useState} from "react";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useSelector} from "react-redux";
|
||||||
import {RootState} from "../../redux/store";
|
import {RootState, useAppDispatch} from "../../redux/store";
|
||||||
import {closeLoadingModal, openLoadingModal, setLoadingText} from "../../features/loadingModal/loadingModalSlice";
|
import {closeLoadingModal, openLoadingModal, setLoadingText} from "../../features/loadingModal/loadingModalSlice";
|
||||||
import {NavigationProp, useNavigation} from "@react-navigation/native";
|
import {NavigationProp, useNavigation} from "@react-navigation/native";
|
||||||
import printingApi from "../../api/printingApi";
|
import printingApi from "../../api/printingApi";
|
||||||
@@ -31,6 +31,8 @@ import {TabNavigatorParamList} from "../MainScreen/MainScreen";
|
|||||||
import {openImageZoomModal, setImages} from "../../features/imageZoomModal/loadingModalSlice";
|
import {openImageZoomModal, setImages} from "../../features/imageZoomModal/loadingModalSlice";
|
||||||
import {OrderStatus, OrderStatusDictionary} from "../../features/ordersFilter/ordersFilterSlice";
|
import {OrderStatus, OrderStatusDictionary} from "../../features/ordersFilter/ordersFilterSlice";
|
||||||
import {openCancelAssemblyModal} from "../../features/cancelAssemblyModal/cancelAssemblyModalSlice";
|
import {openCancelAssemblyModal} from "../../features/cancelAssemblyModal/cancelAssemblyModalSlice";
|
||||||
|
import {fetchBalance, refreshTransactions} from "../../features/balance/balanceSlice";
|
||||||
|
import {showReward} from "../../features/animations/animationsSlice";
|
||||||
|
|
||||||
|
|
||||||
type AssemblyPeriod = {
|
type AssemblyPeriod = {
|
||||||
@@ -68,7 +70,7 @@ type OrderScreenProps = {
|
|||||||
const OrderScreen: FC<OrderScreenProps> = ({order}) => {
|
const OrderScreen: FC<OrderScreenProps> = ({order}) => {
|
||||||
const navigator = useNavigation<NavigationProp<TabNavigatorParamList, 'Barcode'>>();
|
const navigator = useNavigation<NavigationProp<TabNavigatorParamList, 'Barcode'>>();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const assembly = useSelector((state: RootState) => state.assembly.assembly);
|
const assembly = useSelector((state: RootState) => state.assembly.assembly);
|
||||||
const assemblyState = useSelector((state: RootState) => state.assembly.localState);
|
const assemblyState = useSelector((state: RootState) => state.assembly.localState);
|
||||||
const initialOrder = useSelector((state: RootState) => state.assembly.initialOrder);
|
const initialOrder = useSelector((state: RootState) => state.assembly.initialOrder);
|
||||||
@@ -144,12 +146,19 @@ const OrderScreen: FC<OrderScreenProps> = ({order}) => {
|
|||||||
label={"Завершить сборку"}
|
label={"Завершить сборку"}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!assembly) return;
|
if (!assembly) return;
|
||||||
assemblyApi.close(assembly.databaseId).then(({ok, message}) => {
|
assemblyApi.close(assembly.databaseId).then(({ok, message, reward}) => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: ok ? 'success' : 'error',
|
type: ok ? 'success' : 'error',
|
||||||
text1: 'Завершение сборки',
|
text1: 'Завершение сборки',
|
||||||
text2: message
|
text2: message
|
||||||
});
|
});
|
||||||
|
if (ok) {
|
||||||
|
dispatch(showReward({reward}));
|
||||||
|
dispatch(fetchBalance())
|
||||||
|
dispatch(refreshTransactions())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(endAssembly());
|
dispatch(endAssembly());
|
||||||
navigator.navigate('Barcode');
|
navigator.navigate('Barcode');
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,130 +1,54 @@
|
|||||||
import {View, StyleSheet, TouchableOpacity, Image, ScrollView, GestureResponderEvent} from "react-native";
|
import {View, StyleSheet} from "react-native";
|
||||||
import {responsiveFontSize, responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||||
import {blue, gray} from "../../css/colors";
|
import {blue} from "../../css/colors";
|
||||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||||
import DText from "../../components/DText/DText";
|
|
||||||
import DTitle from "../../components/DTitle/DTitle";
|
import DTitle from "../../components/DTitle/DTitle";
|
||||||
import Separator from "../../components/Separator/Separator";
|
import Separator from "../../components/Separator/Separator";
|
||||||
import {BottomSheetModal} from "@gorhom/bottom-sheet";
|
import {useEffect} from "react";
|
||||||
import {useMemo, useRef, useState} from "react";
|
import {useSelector} from "react-redux";
|
||||||
import assemblyApi from "../../api/assemblyApi";
|
import SettingsView from "./SettingsView";
|
||||||
import Toast from "react-native-toast-message";
|
import TransactionsView from "./TransactionView";
|
||||||
import {useDispatch} from "react-redux";
|
import {RootState, useAppDispatch} from "../../redux/store";
|
||||||
import {reset} from "../../features/assembly/assemblySlice";
|
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() {
|
function ProfileScreen() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const balanceState = useSelector((state: RootState) => state.balance);
|
||||||
const snapPoints = useMemo(() => ['25%', '40%'], []);
|
const onTransactionsEndReached = () => {
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
if (balanceState.transactions.isLoading || !balanceState.transactions.hasNext) return;
|
||||||
const dispatch = useDispatch();
|
dispatch(fetchTransactions(balanceState.transactions.currentPage));
|
||||||
|
};
|
||||||
|
const onTransactionsRefresh = () => {
|
||||||
|
dispatch(refreshTransactions());
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchBalance());
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerInfo}>
|
<View style={styles.headerInfo}>
|
||||||
<DTitle style={styles.headerText}>Ваш баланс: 228 руб</DTitle>
|
<DTitle style={styles.headerText}>Ваш
|
||||||
<DTitle style={styles.headerText}>Собрано товаров: 1337 шт</DTitle>
|
баланс: {formatBalanceNumber(balanceState.balance, false)} руб</DTitle>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.actionsCarouselContainer}>
|
<View style={styles.actionsCarouselContainer}>
|
||||||
<ScrollView horizontal={true}
|
<SettingsView/>
|
||||||
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>
|
|
||||||
</View>
|
</View>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<View style={styles.historyContainer}>
|
<View style={styles.historyContainer}>
|
||||||
<DTitle>История операций</DTitle>
|
<DTitle>История операций</DTitle>
|
||||||
<ScrollView
|
<TransactionsView
|
||||||
showsHorizontalScrollIndicator={false}
|
onRefresh={onTransactionsRefresh}
|
||||||
showsVerticalScrollIndicator={false}
|
isLoading={balanceState.transactions.isLoading}
|
||||||
style={styles.historyElements}
|
transactions={balanceState.transactions.items}
|
||||||
contentContainerStyle={styles.historyElementsContainer}>
|
onEndReached={onTransactionsEndReached}/>
|
||||||
<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>
|
|
||||||
</View>
|
</View>
|
||||||
</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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -151,7 +75,8 @@ const styles = StyleSheet.create({
|
|||||||
historyContainer: {
|
historyContainer: {
|
||||||
paddingVertical: responsiveHeight(2),
|
paddingVertical: responsiveHeight(2),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
// backgroundColor: "red"
|
gap: responsiveHeight(1),
|
||||||
|
marginBottom: responsiveHeight(6)
|
||||||
},
|
},
|
||||||
historyElements: {
|
historyElements: {
|
||||||
marginTop: responsiveHeight(3),
|
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