first balance

This commit is contained in:
2024-02-18 19:51:22 +03:00
parent 7bca25a30c
commit 9010574b59
10 changed files with 295 additions and 0 deletions

1
.env Normal file
View File

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

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

@@ -0,0 +1,12 @@
import {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;
}
}
export default balanceApi;

View File

@@ -0,0 +1,25 @@
import {createContext, FC, ReactNode, useContext} from "react";
type ApiState = {
onLogout: () => void;
};
const apiContext = createContext<ApiState | undefined>(undefined);
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,36 @@
import {BalanceTransaction} from "../../types/balance";
import {createAsyncThunk, createSlice, PayloadAction} from "@reduxjs/toolkit";
import {RootState} from "../../redux/store";
const name = 'balance';
interface BalanceState {
transactions: BalanceTransaction[];
balance: number;
page: number;
loading: boolean;
}
const initialState: BalanceState = {
transactions: [],
balance: 0,
page: 1,
loading: false
}
export const balanceSlice = createSlice({
name: name,
initialState,
reducers: {
appendTransactions: (state, payload: PayloadAction<BalanceTransaction[]>) => {
state.transactions.push(...payload.payload);
state.page = state.page + 1;
state.loading = false
},
setIsLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
}
},
})
export const {appendTransactions, setIsLoading} = balanceSlice.actions;
export default balanceSlice.reducer;

View File

@@ -0,0 +1,15 @@
import {createApi} from '@reduxjs/toolkit/query/react'
import {axiosBaseQuery} from "../../redux/general";
import {EndpointBuilder} from "@reduxjs/toolkit/dist/query/endpointDefinitions";
import {BalanceTransaction} from "../../types/balance";
export const balanceApi = createApi({
reducerPath: 'balanceApi',
baseQuery: axiosBaseQuery(),
endpoints: (builder) => ({
getTransactions: builder.query({query: () => ({url: "/transactions", method: 'get'})})
})
})
export const {useGetTransactionsQuery} = balanceApi;

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

@@ -0,0 +1,85 @@
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>{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",
},
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,71 @@
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";
type TransactionElementProps = {
transaction: BalanceTransaction;
}
const TransactionElement: FC<TransactionElementProps> = ({transaction}) => {
const formatNumber = (n: number): string => n >= 0 ? `+${n}` : `${n}`;
return (
<View style={styles.historyElementContainer}>
<DText style={{
fontSize: responsiveFontSize(2),
fontWeight: "500"
}}>{formatNumber(transaction.amount)} руб</DText>
<DText style={{
fontSize: responsiveFontSize(1.5),
color: gray,
fontWeight: "400"
}}>{transaction.description}</DText>
</View>
)
}
type TransactionsViewProps = {
transactions: BalanceTransaction[];
onEndReached: () => void;
}
const TransactionsView: FC<TransactionsViewProps> = ({transactions, onEndReached}) => {
console.log('----------------------------------')
console.log(JSON.stringify(transactions, null, 2))
return (
<FlashList
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}
>
</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 = () => {
}

14
src/types/balance.ts Normal file
View File

@@ -0,0 +1,14 @@
export enum BalanceTransactionType {
TOP_UP,
WITHDRAW
}
export type BalanceTransaction = {
id: number;
type: BalanceTransactionType;
userId: number;
amount: number;
description: string;
jsonData?: object;
createdAt: string;
}