Compare commits

...

2 Commits

Author SHA1 Message Date
a6531ca88a feat: reward 2024-02-23 13:34:06 +03:00
9010574b59 first balance 2024-02-18 19:51:22 +03:00
24 changed files with 564 additions and 168 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_DEV_MODE=true

View File

@@ -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",

View File

@@ -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",

View File

@@ -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/>
<CommonPage/>
</BottomSheetModalProvider>
</GestureHandlerRootView>

View File

@@ -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;

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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;

View File

@@ -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}) => {

View 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>
)
}

View 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;

View 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
View 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,
},
}
}
}

View File

@@ -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
},
});

View File

@@ -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/>

View File

@@ -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: "",

View File

@@ -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');
})

View File

@@ -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),

View 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;

View 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;

View File

@@ -0,0 +1,3 @@
const useBalance = () => {
}

18
src/types/balance.ts Normal file
View 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
View 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);
}