first balance
This commit is contained in:
12
src/api/balanceApi.ts
Normal file
12
src/api/balanceApi.ts
Normal 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;
|
||||||
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);
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
src/features/balance/balanceSlice.ts
Normal file
36
src/features/balance/balanceSlice.ts
Normal 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;
|
||||||
15
src/features/balance/service.ts
Normal file
15
src/features/balance/service.ts
Normal 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
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/screens/ProfileScreen/SettingsView.tsx
Normal file
85
src/screens/ProfileScreen/SettingsView.tsx
Normal 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;
|
||||||
71
src/screens/ProfileScreen/TransactionView.tsx
Normal file
71
src/screens/ProfileScreen/TransactionView.tsx
Normal 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;
|
||||||
3
src/screens/ProfileScreen/useBalance.tsx
Normal file
3
src/screens/ProfileScreen/useBalance.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const useBalance = () => {
|
||||||
|
|
||||||
|
}
|
||||||
14
src/types/balance.ts
Normal file
14
src/types/balance.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user