Tài sản
- API Service
- Component
/src/api/AssetService.ts
import { AssetActionPostModel } from "../model/asset/AssetActionPostModel";
import { AssetActionResponseModel } from "../model/asset/AssetActionReponseModel";
import { AssetResponseModel } from "../model/asset/AssetResponseModel";
import { ApiResponse } from "../model/base/ApiResponseModel";
import { Pagination } from "../model/pagination/Pagination";
import { HttpService } from "./HttpService";
class AssetApi extends HttpService {
constructor() {
super();
this.baseurl = this.publicFMReApiUrl
}
GetAsset = async (clientId: string, projectId: string, pageIndex: number) => {
const res = await this.Get(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/page-${pageIndex}`
);
if (res.status !== 200) {
let result = new ApiResponse<Pagination<AssetResponseModel>>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<Pagination<AssetResponseModel>> = await res.json();
return json;
};
GetAssetBySpace = async (clientId: string, projectId: string, spaceId: string, pageIndex: number) => {
const res = await this.Get(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/space-${spaceId}/page-${pageIndex}`
);
if (res.status !== 200) {
let result = new ApiResponse<Pagination<AssetResponseModel>>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<Pagination<AssetResponseModel>> = await res.json();
return json;
};
SearchAsset = async (projectId: string, text: string, pageIndex: number) => {
const res = await this.Get(
`/api/fm-mobile/v1/p-${projectId}/search/asset/${text}/${pageIndex}`
);
if (res.status !== 200) {
let result = new ApiResponse<Pagination<AssetResponseModel>>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<Pagination<AssetResponseModel>> = await res.json();
return json;
};
SearchAssetBySpace = async (clientId: string, projectId: string, spaceId:string, text: string, pageIndex: number) => {
const res = await this.Get(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/space-${spaceId}/search/${text}`
);
if (res.status !== 200) {
let result = new ApiResponse<Pagination<AssetResponseModel>>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<Pagination<AssetResponseModel>> = await res.json();
return json;
};
GetAssetActions = async (clientId: String, projectId: String, assetId: String) => {
const res = await this.Get(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/actions/${assetId}`
);
if (res.status !== 200) {
let result = new ApiResponse<AssetActionResponseModel[]>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<AssetActionResponseModel[]> = await res.json();
return json;
};
FindAsset = async (clientId: string, projectId: string, assetCode: string) => {
const res = await this.Get(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/${assetCode}`
);
if (res.status !== 200) {
let result = new ApiResponse<AssetResponseModel>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<AssetResponseModel> = await res.json();
return json;
};
PostAction = async (clientId: string, projectId: string, postModel: AssetActionPostModel) => {
const res = await this.Post(
`/api/fm-mobile/v1/c-${clientId}/p-${projectId}/assets/actions`, postModel);
if (res.status !== 200) {
let result = new ApiResponse<boolean>();
result.statusCode = res.status;
return result;
}
const json: ApiResponse<boolean> = await res.json();
return json;
};
}
const assetApi = new AssetApi();
export default assetApi;
/src/components/asset/AssetPage.tsx
import { View, Text, ScrollView, RefreshControl, Pressable, Platform, Image } from 'react-native'
import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'
import { GlobalStyles } from '../../styles/GlobalStyles'
import { Checkbox, TouchableRipple } from 'react-native-paper'
import { NavigationContainerRef } from '../../navigation/GlobalNavigationContainer'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import CustomInput from '../global/CustomInput'
import { GlobalComponentsState } from '../../model/context/GlobalComponentsStateModel'
import { AssetReduxModel } from '../../model/redux/AssetReduxModel'
import { useDispatch, useSelector } from 'react-redux'
import AppContext from '../../context/AppContext'
import { AuthenticateReduxModel } from '../../model/redux/AuthenticateReduxModel'
import { PutAsset, SetAssetData, SetAssetIndex, SetSearchAssetData, SetSearchAssetIndex, SetSelectedAsset } from '../../redux/actions/AssetAction'
import assetApi from '../../api/AssetService'
import { Asset } from '../../model/asset/Asset'
import CustomImage from '../global/CustomImage'
import { debounceTime, Subject } from 'rxjs'
import { useTranslation } from 'react-i18next'
type AssetPageProps = PropsWithChildren<{
navigation?: any;
route?: any;
showHeader: boolean;
useForSelect?: boolean;
}>;
export default function AssetPage(props: AssetPageProps) {
const globalContext: GlobalComponentsState = useContext(AppContext);
const authenticateReduxData: AuthenticateReduxModel = useSelector(
(state: any) => state.AuthenticateReducer,
);
const assetReduxModelData: AssetReduxModel = useSelector(
(state: any) => state.AssetReducer,
);
const { t } = useTranslation();
const dispatch = useDispatch<any>();
const [searchContent, setSearchContent] = useState('');
const [onSearch, setOnSearch] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
const changeHandler = new Subject<string>();
const unsubscribe = changeHandler
.asObservable()
.pipe(debounceTime(800))
.subscribe((val: string) => {
if (val && val.length > 2) {
retrieveSearchData(val);
setSearchContent(val);
setOnSearch(true);
} else {
retrieveData();
setOnSearch(false);
}
});
useEffect(() => {
if (assetReduxModelData.assets.total == 0)
retrieveData();
if (props.route && props.route.params.keyword) {
retrieveSearchData(props.route.params.keyword);
setSearchContent(props.route.params.keyword);
setOnSearch(true);
}
return () => {
if (unsubscribe) unsubscribe.unsubscribe();
};
}, [authenticateReduxData.selectedProject]);
//#region Retrieve Data
const retrieveData = async () => {
dispatch(SetAssetIndex(0));
if (
authenticateReduxData.selectedClient &&
authenticateReduxData.selectedProject
) {
let res = await assetApi.GetAsset(authenticateReduxData.selectedClient.id, authenticateReduxData.selectedProject.id, 0);
if (res.successful) {
if (res.result) {
let items = res.result.items.map(x => new Asset(x));
dispatch(SetAssetData(res.result.total, items));
if (items.length == 0) {
setIsEmpty(true);
}
} else {
globalContext.openSnackbar(res.errorMessage);
}
} else {
globalContext.openSnackbar(`Retrieve asset data: ${res.statusCode.toString()}`);
}
}
};
const retrieveMoreData = async () => {
if (
authenticateReduxData.selectedClient &&
authenticateReduxData.selectedProject &&
assetReduxModelData.assets.items.length <
assetReduxModelData.assets.total
) {
dispatch(SetAssetIndex(assetReduxModelData.assetPageIndex + 1));
let res = await assetApi.GetAsset(
authenticateReduxData.selectedClient.id,
authenticateReduxData.selectedProject.id,
assetReduxModelData.assetPageIndex + 1,
);
if (res.successful) {
if (res.result) {
let items = res.result.items.map(x => new Asset(x));
let all = assetReduxModelData.assets.items.concat(items);
dispatch(SetAssetData(res.result.total, all));
} else {
globalContext.openSnackbar(res.errorMessage);
}
} else {
globalContext.openSnackbar(`Send data: ${res.statusCode.toString()}`);
}
}
};
//#endregion
//#region Retrieve Search Data
const retrieveSearchData = async (text: string) => {
dispatch(SetSearchAssetIndex(0));
if (
authenticateReduxData.selectedClient &&
authenticateReduxData.selectedProject
) {
let res = await assetApi.SearchAsset(authenticateReduxData.selectedProject.id, text, 0);
if (res.successful) {
if (res.result) {
let items = res.result.items.map(x => new Asset(x));
dispatch(SetSearchAssetData(res.result.total, items));
} else {
globalContext.openSnackbar(res.errorMessage);
}
} else {
globalContext.openSnackbar(`Retrieve asset data: ${res.statusCode.toString()}`);
}
}
};
const retrieveMoreSearchData = async (text: string) => {
if (
authenticateReduxData.selectedClient &&
authenticateReduxData.selectedProject &&
assetReduxModelData.searchAssets.items.length <
assetReduxModelData.searchAssets.total
) {
dispatch(SetSearchAssetIndex(assetReduxModelData.searchAssetPageIndex + 1));
let res = await assetApi.SearchAsset(
authenticateReduxData.selectedProject.id,
text,
assetReduxModelData.searchAssetPageIndex + 1,
);
if (res.successful) {
if (res.result) {
let items = res.result.items.map(x => new Asset(x));
let all = assetReduxModelData.searchAssets.items.concat(items);
dispatch(SetSearchAssetData(res.result.total, all));
} else {
globalContext.openSnackbar(res.errorMessage);
}
} else {
globalContext.openSnackbar(`Send data: ${res.statusCode.toString()}`);
}
}
};
//#endregion
const [refreshing, setRefreshing] = useState(false);
const onRefresh = async () => {
setRefreshing(true);
retrieveData();
setRefreshing(false);
};
function handleInfinityScroll(event: any) {
let mHeight = event.nativeEvent.layoutMeasurement.height;
let cSize = event.nativeEvent.contentSize.height;
let Y = event.nativeEvent.contentOffset.y;
if (Math.ceil(mHeight + Y) >= cSize) return true;
return false;
}
return (
<View style={[GlobalStyles.body, GlobalStyles.displayFlex, GlobalStyles.flexDirectionColumn, GlobalStyles.backGroundColorWhite]}>
{(!props.showHeader) ? null : <View style={[GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter, GlobalStyles.justifyContentSpaceBetween]}>
<View style={[GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter]}>
<TouchableRipple borderless={Platform.OS === 'android' ? true : false} onPress={() => {
NavigationContainerRef.goBack()
}} style={{ padding: 10, borderRadius: 20 }}
>
<Icon
name='arrow-left'
color='black'
size={24}
/>
</TouchableRipple>
<Text style={{ fontSize: 20, fontWeight: '600' }}>{t("Asset.Asset")}</Text>
</View>
<Text style={{ paddingRight: 10 }}>({onSearch ? assetReduxModelData.searchAssets.total : assetReduxModelData.assets.total})</Text>
</View>}
<CustomInput value={(props.route && props.route.params.keyword) && props.route.params.keyword} onChangeText={(text) => {
changeHandler.next(text);
}} affixIcon='magnify' style={{
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.2)',
}} placeHolderColor='#303030' placeholder={t("Search.Search")}></CustomInput>
{Platform.OS === 'android' ?
<ScrollView
onScroll={async event => {
if (handleInfinityScroll(event)) {
onSearch ? await retrieveMoreSearchData(searchContent) : await retrieveMoreData();
}
}}
scrollEventThrottle={1000}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
style={{ paddingTop: 10 }}>
{onSearch ? (
assetReduxModelData.searchAssets.items.length > 0 && assetReduxModelData.searchAssets.items.map((x: Asset, index: number) => {
return <Pressable style={[
GlobalStyles.flexDirectionRow,
GlobalStyles.justifyContentSpaceBetween,
GlobalStyles.shadow,
GlobalStyles.backGroundColorWhite,
GlobalStyles.alignItemsCenter,
{ paddingTop: 2.5 },
{ marginBottom: 5, padding: 5, borderRadius: 10 }
]} key={index} onPress={() => { dispatch(SetSelectedAsset(x)); NavigationContainerRef.navigate("AssetModal") }}>
<View style={[GlobalStyles.spacer, GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter]}>
{props.useForSelect && <Checkbox.Android onPress={() => { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) }} status={x.isSelected ? 'checked' : 'unchecked'}></Checkbox.Android>
}
<View>
<Text>{x.code}</Text>
<Text>{x.name}</Text>
<Text>{x.space?.name}</Text>
</View>
</View>
{x.imageRootPathUrl && <CustomImage maxWidth={100} uri={x.imageRootPathUrl}></CustomImage>}
</Pressable>
})
) :
(
assetReduxModelData.assets.items.length > 0 && assetReduxModelData.assets.items.map((x: Asset, index: number) => {
return <Pressable style={[
GlobalStyles.flexDirectionRow,
GlobalStyles.justifyContentSpaceBetween,
GlobalStyles.shadow,
GlobalStyles.backGroundColorWhite,
GlobalStyles.alignItemsCenter,
{ paddingTop: 2.5 },
{ marginBottom: 5, padding: 5, borderRadius: 10 }
]} key={index} onPress={() => { if (props.useForSelect) { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) } else { dispatch(SetSelectedAsset(x)); NavigationContainerRef.navigate("AssetModal") } }}>
<View style={[GlobalStyles.spacer, GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter]}>
{props.useForSelect && <Checkbox.Android onPress={() => { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) }} status={x.isSelected ? 'checked' : 'unchecked'}></Checkbox.Android>
}
<View>
<Text>{x.code}</Text>
<Text>{x.name}</Text>
<Text>{x.space?.name}</Text>
</View>
</View>
{x.imageRootPathUrl && <CustomImage maxWidth={100} uri={x.imageRootPathUrl}></CustomImage>}
</Pressable>
})
)}
{isEmpty && <View
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}>
<Image style={{ height: 150, width: 250 }} source={require('../../../assets/img/empty.png')}></Image>
<Text style={{ fontSize: 18 }}>{t("Asset.Empty")}</Text>
</View>}
</ScrollView> : <ScrollView onTouchEnd={async (e) => { onSearch ? await retrieveMoreSearchData(searchContent) : await retrieveMoreData(); }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
style={{ paddingTop: 10 }}>
{onSearch ? (
assetReduxModelData.searchAssets.items.length > 0 && assetReduxModelData.searchAssets.items.map((x: Asset, index: number) => {
return <Pressable style={[
GlobalStyles.flexDirectionRow,
GlobalStyles.justifyContentSpaceBetween,
GlobalStyles.shadow,
GlobalStyles.backGroundColorWhite,
GlobalStyles.alignItemsCenter,
{ paddingTop: 2.5 },
{ marginBottom: 5, padding: 5, borderRadius: 10 }
]} key={index} onPress={() => { dispatch(SetSelectedAsset(x)); NavigationContainerRef.navigate("AssetModal") }}>
<View style={[GlobalStyles.spacer, GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter]}>
{props.useForSelect && <Checkbox.Android onPress={() => { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) }} status={x.isSelected ? 'checked' : 'unchecked'}></Checkbox.Android>
}
<View>
<Text>{x.code}</Text>
<Text>{x.name}</Text>
<Text>{x.space?.name}</Text>
</View>
</View>
{x.imageRootPathUrl && <CustomImage maxWidth={100} uri={x.imageRootPathUrl}></CustomImage>}
</Pressable>
})
) :
(
assetReduxModelData.assets.items.length > 0 && assetReduxModelData.assets.items.map((x: Asset, index: number) => {
return <Pressable style={[
GlobalStyles.flexDirectionRow,
GlobalStyles.justifyContentSpaceBetween,
GlobalStyles.shadow,
GlobalStyles.backGroundColorWhite,
GlobalStyles.alignItemsCenter,
{ paddingTop: 2.5 },
{ marginBottom: 5, padding: 5, borderRadius: 10 }
]} key={index} onPress={() => { if (props.useForSelect) { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) } else { dispatch(SetSelectedAsset(x)); NavigationContainerRef.navigate("AssetModal") } }}>
<View style={[GlobalStyles.spacer, GlobalStyles.flexDirectionRow, GlobalStyles.alignItemsCenter]}>
{props.useForSelect && <Checkbox.Android onPress={() => { x.isSelected = !x.isSelected; dispatch(PutAsset(x)) }} status={x.isSelected ? 'checked' : 'unchecked'}></Checkbox.Android>
}
<View>
<Text>{x.code}</Text>
<Text>{x.name}</Text>
<Text>{x.space?.name}</Text>
</View>
</View>
{x.imageRootPathUrl && <CustomImage maxWidth={100} uri={x.imageRootPathUrl}></CustomImage>}
</Pressable>
})
)}
{isEmpty && <View
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}>
<Image style={{ height: 150, width: 250 }} source={require('../../../assets/img/empty.png')}></Image>
<Text style={{ fontSize: 18 }}>This project do not contains any asset.</Text>
</View>}
</ScrollView>}
</View>
)
}