Ringkasan Perubahan
1. Pembuatan dan Pembaruan Komponen Layar Investasi dengan Sistem Pagination
ScreenMyHolding.tsx
- Diperbarui dari sistem loading data statis ke sistem pagination dinamis
- Menggunakan hook usePagination untuk manajemen data
- Mengganti ViewWrapper dengan NewWrapper untuk tampilan yang lebih modern
- Menambahkan fitur pull-to-refresh dan infinite scroll
- Menggunakan helper pagination dari createPaginationComponents
ScreenInvestor.tsx
- Dibuat baru dengan sistem pagination
- Menggunakan hook usePagination untuk manajemen data
- Menggunakan NewWrapper sebagai komponen dasar
- Menambahkan fitur pull-to-refresh dan infinite scroll
ScreenRecapOfNews.tsx
- Dibuat baru dengan sistem pagination
- Menggunakan hook usePagination untuk manajemen data
- Menggunakan NewWrapper sebagai komponen dasar
- Menambahkan fitur pull-to-refresh dan infinite scroll
ScreenListOfNews.tsx
- Dibuat baru dengan sistem pagination
- Menggunakan hook usePagination untuk manajemen data
- Menggunakan NewWrapper sebagai komponen dasar
- Menambahkan fitur pull-to-refresh dan infinite scroll
2. Perubahan pada Fungsi-Fungsi API untuk Mendukung Pagination
apiInvestmentGetAll
- Sudah mendukung parameter page dengan default "1"
apiInvestmentGetInvestorById
- Ditambahkan parameter page dengan default "1"
- Memungkinkan pengambilan data investor secara bertahap
apiInvestmentGetNews
- Ditambahkan parameter page dengan default "1"
- Memungkinkan pengambilan data berita secara bertahap
3. Pembaruan File-File di Direktori app/ untuk Menggunakan Komponen-Komponen Baru
app/(application)/(user)/investment/[id]/investor.tsx
- Diperbarui untuk menggunakan komponen Investment_ScreenInvestor
- Menghilangkan logika lokal dan menggantinya dengan komponen modular
app/(application)/(user)/investment/[id]/(news)/recap-of-news.tsx
- Diperbarui untuk menggunakan komponen Investment_ScreenRecapOfNews
- Menghilangkan logika lokal dan menggantinya dengan komponen modular
app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx
- Diperbarui untuk menggunakan komponen Investment_ScreenListOfNews
- Menghilangkan logika lokal dan menggantinya dengan komponen modular
4. Implementasi Sistem Pagination
usePagination Hook
- Digunakan secara konsisten di semua komponen layar investasi
- Menyediakan fitur load more dan refresh
- Mengelola state loading, refreshing, dan data
NewWrapper Component
- Digunakan sebagai pengganti ViewWrapper
- Menyediakan dukungan bawaan untuk FlatList
- Menyediakan dukungan untuk refreshControl dan onEndReached
Pagination Helpers
- Menggunakan createPaginationComponents untuk menghasilkan komponen-komponen pagination
- Menyediakan skeleton loading saat data sedang dimuat
- Menyediakan pesan kosong saat tidak ada data
5. Manfaat dari Perubahan Ini
1. Performa Lebih Baik: Dengan pagination, hanya sejumlah kecil data yang dimuat pada satu waktu, meningkatkan
performa aplikasi
2. Pengalaman Pengguna Lebih Baik: Fitur pull-to-refresh dan infinite scroll membuat navigasi lebih intuitif
3. Kode Lebih Modular: Komponen-komponen baru dapat digunakan kembali dan dipelihara lebih mudah
4. Struktur Kode Lebih Rapi: Pemisahan antara logika tampilan dan logika data membuat kode lebih terorganisir
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
169
QWEN.md
169
QWEN.md
@@ -0,0 +1,169 @@
|
|||||||
|
# HIPMI Mobile Application - Development Context
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named "HIPMI Badung Connect" and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Architecture**: File-based routing with Expo Router
|
||||||
|
- **State Management**: Context API
|
||||||
|
- **UI Components**: React Native Paper, custom components
|
||||||
|
- **Maps Integration**: Mapbox Maps for React Native
|
||||||
|
- **Push Notifications**: React Native Firebase Messaging
|
||||||
|
- **Build System**: Metro bundler
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
hipmi-mobile/
|
||||||
|
├── app/ # Main application screens and routing
|
||||||
|
│ ├── _layout.tsx # Root layout component
|
||||||
|
│ ├── index.tsx # Entry point (Login screen)
|
||||||
|
│ └── ...
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
├── context/ # State management (AuthContext)
|
||||||
|
├── screens/ # Screen components organized by feature
|
||||||
|
│ ├── Admin/ # Admin panel screens
|
||||||
|
│ ├── Authentication/ # Login, registration flows
|
||||||
|
│ ├── Collaboration/ # Collaboration features
|
||||||
|
│ ├── Event/ # Event management
|
||||||
|
│ ├── Forum/ # Forum functionality
|
||||||
|
│ ├── Home/ # Home screen
|
||||||
|
│ ├── Maps/ # Map integration
|
||||||
|
│ ├── Profile/ # User profile
|
||||||
|
│ └── ...
|
||||||
|
├── assets/ # Images, icons, and static assets
|
||||||
|
├── constants/ # Constants and configuration values
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── lib/ # Utility libraries
|
||||||
|
├── navigation/ # Navigation configuration
|
||||||
|
├── service/ # API services and business logic
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
└── utils/ # Helper functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js (with bun as the package manager)
|
||||||
|
- Expo CLI
|
||||||
|
- iOS Simulator or Android Emulator (for native builds)
|
||||||
|
|
||||||
|
### Setup and Development
|
||||||
|
|
||||||
|
1. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run Development Server**
|
||||||
|
```bash
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
Or use the shorthand:
|
||||||
|
```bash
|
||||||
|
bunx expo start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Platform-Specific Commands**
|
||||||
|
- iOS: `bun run ios` or `bunx expo start --ios`
|
||||||
|
- Android: `bun run android` or `bunx expo start --android`
|
||||||
|
- Web: `bun run web` or `bunx expo start --web`
|
||||||
|
|
||||||
|
4. **Linting**
|
||||||
|
```bash
|
||||||
|
bun run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
The application uses environment variables defined in the app.config.js file:
|
||||||
|
- `API_BASE_URL`: Base URL for API endpoints
|
||||||
|
- `BASE_URL`: Base application URL
|
||||||
|
- `DEEP_LINK_URL`: URL for deep linking functionality
|
||||||
|
|
||||||
|
### EAS Build Configuration
|
||||||
|
The project uses Expo Application Services (EAS) for building and deploying:
|
||||||
|
- Development builds with development client
|
||||||
|
- Preview builds for internal distribution
|
||||||
|
- Production builds for app stores
|
||||||
|
|
||||||
|
## Features and Functionality
|
||||||
|
|
||||||
|
The application appears to include several key modules:
|
||||||
|
- **Authentication**: Login, registration, and verification flows
|
||||||
|
- **Admin Panel**: Administrative functions
|
||||||
|
- **Collaboration**: Tools for member collaboration
|
||||||
|
- **Events**: Event management and calendar
|
||||||
|
- **Forum**: Discussion forums
|
||||||
|
- **Maps**: Location-based services with Mapbox integration
|
||||||
|
- **Donations**: Donation functionality
|
||||||
|
- **Job Board**: Employment opportunities
|
||||||
|
- **Investment**: Investment-related features
|
||||||
|
- **Voting**: Voting systems
|
||||||
|
- **Portfolio**: Member portfolio showcase
|
||||||
|
- **Notifications**: Push notifications via Firebase
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
- TypeScript is used throughout the project for type safety
|
||||||
|
- Component-based architecture with reusable components
|
||||||
|
- Context API for state management
|
||||||
|
- File-based routing with Expo Router
|
||||||
|
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Linting is configured with ESLint
|
||||||
|
- Standard Expo linting configuration is used
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Firebase is integrated for authentication and messaging
|
||||||
|
- Camera and location permissions are properly configured
|
||||||
|
- Deep linking is secured with app domain associations
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- `@react-navigation/*`: Navigation solution for React Native
|
||||||
|
- `@react-native-firebase/*`: Firebase integration for React Native
|
||||||
|
- `@rnmapbox/maps`: Mapbox integration for React Native
|
||||||
|
- `expo-router`: File-based routing for Expo applications
|
||||||
|
- `react-native-paper`: Material Design components for React Native
|
||||||
|
- `react-native-toast-message`: Toast notifications
|
||||||
|
- `react-native-otp-entry`: OTP input components
|
||||||
|
- `react-native-qrcode-svg`: QR code generation
|
||||||
|
|
||||||
|
### Development Dependencies
|
||||||
|
- `@types/*`: TypeScript type definitions
|
||||||
|
- `eslint-config-expo`: Expo-specific ESLint configuration
|
||||||
|
- `typescript`: Type checking
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
The application is configured to support:
|
||||||
|
- **iOS**: With tablet support and proper permissions
|
||||||
|
- **Android**: With adaptive icons and intent filters for deep linking
|
||||||
|
- **Web**: Static output configuration for web deployment
|
||||||
|
|
||||||
|
## Special Configurations
|
||||||
|
|
||||||
|
### iOS Configuration
|
||||||
|
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||||
|
- Supports tablets
|
||||||
|
- Google Services integration
|
||||||
|
- Location permission handling
|
||||||
|
- Associated domains for deep linking
|
||||||
|
|
||||||
|
### Android Configuration
|
||||||
|
- Package name: `com.bip.hipmimobileapp`
|
||||||
|
- Adaptive icons
|
||||||
|
- Edge-to-edge display enabled
|
||||||
|
- Intent filters for HTTPS deep linking
|
||||||
|
- Google Services integration
|
||||||
|
|
||||||
|
### Maps Integration
|
||||||
|
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||||
|
|
||||||
|
### Push Notifications
|
||||||
|
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||||
@@ -1,83 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenMyHolding from "@/screens/Invesment/ScreenMyHolding";
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
ProgressCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function InvestmentMyHolding() {
|
export default function InvestmentMyHolding() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [user?.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetAll({
|
|
||||||
category: "my-holding",
|
|
||||||
authorId: user?.id,
|
|
||||||
});
|
|
||||||
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter>
|
<>
|
||||||
{loadingList ? (
|
<Investment_ScreenMyHolding />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
list?.map((item, index) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingTop={7}
|
|
||||||
paddingBottom={7}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
|
||||||
<TextCustom>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
|
|
||||||
<ProgressCustom
|
|
||||||
label={`${item.progress}%`}
|
|
||||||
value={Number(item.progress)}
|
|
||||||
size="lg"
|
|
||||||
animated
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import Investment_ScreenRecap from "@/screens/Invesment/Document/ScreenRecap";
|
import Investment_ScreenRecapOfDocument from "@/screens/Invesment/Document/ScreenRecapOfDocument";
|
||||||
|
|
||||||
export default function InvestmentRecapOfDocument() {
|
export default function InvestmentRecapOfDocument() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Investment_ScreenRecap />
|
<Investment_ScreenRecapOfDocument />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenListOfNews from "@/screens/Invesment/News/ScreenListOfNews";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentListOfNews() {
|
export default function InvestmentListOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetNews({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-news",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenListOfNews investmentId={id as string} />
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Daftar Berita",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingBlock={5}
|
|
||||||
href={`/investment/[id]/(news)/${item.id}`}
|
|
||||||
>
|
|
||||||
<TextCustom bold>{item.title}</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/investment/${id}/add-news`,
|
|
||||||
icon: <IconPlus />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
router.push(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenRecapOfNews from "@/screens/Invesment/News/ScreenRecapOfNews";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DotButton,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentRecapOfNews() {
|
export default function InvestmentRecapOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetNews({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-news",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenRecapOfNews investmentId={id as string} />
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Rekap Berita",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingBlock={5}
|
|
||||||
href={`/investment/[id]/(news)/${item.id}`}
|
|
||||||
>
|
|
||||||
<TextCustom bold>{item.title}</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/investment/${id}/add-news`,
|
|
||||||
icon: <IconPlus />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
router.push(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
|
import Investment_ScreenInvoice from "@/screens/Invesment/ScreenInvoice";
|
||||||
|
|
||||||
export default function InvestmentInvoice() {
|
export default function InvestmentInvoice() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Investment_ScreenTransaction />
|
<Investment_ScreenInvoice />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenInvestor from "@/screens/Invesment/ScreenInvestor";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
AvatarUsernameAndOtherComponent,
|
|
||||||
BoxWithHeaderSection,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { apiInvestmentGetInvestorById } from "@/service/api-client/api-investment";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentInvestor() {
|
export default function InvestmentInvestor() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetInvestorById({
|
|
||||||
id: id as string,
|
|
||||||
})
|
|
||||||
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenInvestor investmentId={id as string} />
|
||||||
<ViewWrapper>
|
|
||||||
{loadingList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BoxWithHeaderSection key={index}>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.Author?.Profile?.imageId}
|
|
||||||
name={item?.Author?.username}
|
|
||||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
|
||||||
/>
|
|
||||||
<TextCustom bold>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
</BoxWithHeaderSection>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
<!-- ===================== Start Penerapan Pagination ===================== -->
|
<!-- ===================== Start Penerapan Pagination ===================== -->
|
||||||
|
|
||||||
|
File source: app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx
|
||||||
|
Folder tujuan: screens/Invesment
|
||||||
|
Nama file utama: ScreenListOfNews.tsx
|
||||||
|
|
||||||
|
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Investment_ScreenListOfNews" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||||
|
Selanjutnya terapkan pagination pada file "Nama file utama"
|
||||||
|
|
||||||
|
Function fecth: apiInvestmentGetNews
|
||||||
|
File function fetch: service/api-client/api-investment.ts
|
||||||
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
|
Terapkan pagination pada file "Nama file utama"
|
||||||
|
Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
|
||||||
|
|
||||||
|
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||||
|
|
||||||
|
Perbaiki fetch "Function fecth" , pada file "File function fetch"
|
||||||
|
Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
||||||
|
|
||||||
|
|
||||||
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||||
|
|
||||||
|
<!-- Additional Prompt -->
|
||||||
|
File refrensi: screens/Event/ScreenStatus.tsx
|
||||||
|
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan Pagination ===================== -->
|
||||||
|
|
||||||
|
<!-- ===================== Start Penerapan NewWrapper ===================== -->
|
||||||
File utama: screens/Invesment/ScreenTransaction.tsx
|
File utama: screens/Invesment/ScreenTransaction.tsx
|
||||||
Function fecth: apiInvestmentGetInvoice
|
Function fecth: apiInvestmentGetInvoice
|
||||||
File function fetch: service/api-client/api-investment.ts
|
File function fetch: service/api-client/api-investment.ts
|
||||||
@@ -15,11 +44,7 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
|||||||
|
|
||||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||||
|
|
||||||
<!-- Additional Prompt -->
|
<!-- ===================== End Penerapan NewWrapper ===================== -->
|
||||||
File refrensi: screens/Event/ScreenStatus.tsx
|
|
||||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
|
||||||
|
|
||||||
<!-- ===================== End Penerapan Pagination ===================== -->
|
|
||||||
|
|
||||||
<!-- Start Penerapan NewWrapper -->
|
<!-- Start Penerapan NewWrapper -->
|
||||||
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
|
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import {
|
|||||||
BackButton,
|
BackButton,
|
||||||
DotButton,
|
DotButton,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid
|
||||||
TextCustom,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
import { IconEdit } from "@/components/_Icon";
|
||||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
import { usePagination } from "@/hooks/use-pagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
|
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
|
||||||
@@ -26,12 +24,11 @@ import {
|
|||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useLocalSearchParams,
|
useLocalSearchParams,
|
||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { RefreshControl } from "react-native";
|
import { RefreshControl } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
export default function Investment_ScreenRecap() {
|
export default function Investment_ScreenRecapOfDocument() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
const [openDrawerBox, setOpenDrawerBox] = useState(false);
|
const [openDrawerBox, setOpenDrawerBox] = useState(false);
|
||||||
13
screens/Invesment/News/BoxNews.tsx
Normal file
13
screens/Invesment/News/BoxNews.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { BaseBox, Spacing, TextCustom } from "@/components";
|
||||||
|
|
||||||
|
export default function Investment_BoxNews({ item }: { item: { id: string; title: string } }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox paddingBlock={5} href={`/investment/[id]/(news)/${item.id}`}>
|
||||||
|
<Spacing height={10} />
|
||||||
|
<TextCustom bold truncate={2}>{item.title}</TextCustom>
|
||||||
|
<Spacing height={10} />
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
screens/Invesment/News/ScreenListOfNews.tsx
Normal file
109
screens/Invesment/News/ScreenListOfNews.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BackButton,
|
||||||
|
BaseBox,
|
||||||
|
DrawerCustom,
|
||||||
|
LoaderCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import Investment_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface InvestmentListOfNewsProps {
|
||||||
|
investmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Investment_ScreenListOfNews({
|
||||||
|
investmentId,
|
||||||
|
}: InvestmentListOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiInvestmentGetNews({
|
||||||
|
id: investmentId,
|
||||||
|
category: "all-news",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: 10, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [investmentId],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [investmentId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Investment_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada berita",
|
||||||
|
skeletonCount: 5,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
loadingFooterText: "Memuat lebih banyak berita...",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Daftar Berita",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/investment/${investmentId}/add-news`,
|
||||||
|
icon: <IconPlus />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
router.push(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
screens/Invesment/News/ScreenRecapOfNews.tsx
Normal file
110
screens/Invesment/News/ScreenRecapOfNews.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BackButton,
|
||||||
|
BaseBox,
|
||||||
|
DotButton,
|
||||||
|
DrawerCustom,
|
||||||
|
LoaderCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
Spacing,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import Investment_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface InvestmentRecapOfNewsProps {
|
||||||
|
investmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Investment_ScreenRecapOfNews({
|
||||||
|
investmentId,
|
||||||
|
}: InvestmentRecapOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiInvestmentGetNews({
|
||||||
|
id: investmentId,
|
||||||
|
category: "all-news",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [investmentId],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [investmentId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Investment_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada berita",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Rekap Berita",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/investment/${investmentId}/add-news`,
|
||||||
|
icon: <IconPlus />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
router.push(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
screens/Invesment/ScreenInvestor.tsx
Normal file
84
screens/Invesment/ScreenInvestor.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
AvatarUsernameAndOtherComponent,
|
||||||
|
BoxWithHeaderSection,
|
||||||
|
CenterCustom,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiInvestmentGetInvestorById } from "@/service/api-client/api-investment";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
interface InvestmentInvestorProps {
|
||||||
|
investmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Investment_ScreenInvestor({
|
||||||
|
investmentId,
|
||||||
|
}: InvestmentInvestorProps) {
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiInvestmentGetInvestorById({
|
||||||
|
id: investmentId,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [investmentId],
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error fetching investors:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: any }) => (
|
||||||
|
<BoxWithHeaderSection>
|
||||||
|
<StackCustom>
|
||||||
|
<AvatarUsernameAndOtherComponent
|
||||||
|
avatar={item?.Author?.Profile?.imageId}
|
||||||
|
name={item?.Author?.username}
|
||||||
|
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
||||||
|
/>
|
||||||
|
<CenterCustom>
|
||||||
|
<TextCustom size="large" bold>
|
||||||
|
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
</CenterCustom>
|
||||||
|
</StackCustom>
|
||||||
|
<Spacing height={10} />
|
||||||
|
</BoxWithHeaderSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada investor",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
230
screens/Invesment/ScreenInvoice.tsx
Normal file
230
screens/Invesment/ScreenInvoice.tsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BaseBox,
|
||||||
|
ButtonCenteredOnly,
|
||||||
|
ButtonCustom,
|
||||||
|
Grid,
|
||||||
|
InformationBox,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
ViewWrapper,
|
||||||
|
} from "@/components";
|
||||||
|
import CopyButton from "@/components/Button/CoyButton";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
|
import {
|
||||||
|
apiInvestmentGetInvoice,
|
||||||
|
apiInvestmentUpdateInvoice,
|
||||||
|
} from "@/service/api-client/api-investment";
|
||||||
|
import { uploadFileService } from "@/service/upload-service";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import pickFile, { IFileData } from "@/utils/pickFile";
|
||||||
|
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
|
export default function Investment_ScreenInvoice() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const [data, setData] = useState<any>({});
|
||||||
|
const [image, setImage] = useState<IFileData>({
|
||||||
|
name: "",
|
||||||
|
uri: "",
|
||||||
|
size: 0,
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
onLoadData();
|
||||||
|
}, [id])
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLoadData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiInvestmentGetInvoice({
|
||||||
|
id: id as string,
|
||||||
|
category: "invoice",
|
||||||
|
});
|
||||||
|
|
||||||
|
setData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlerSubmitUpdate = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const responseUploadImage = await uploadFileService({
|
||||||
|
dirId: DIRECTORY_ID.investasi_bukti_transfer,
|
||||||
|
imageUri: image?.uri,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!responseUploadImage?.data?.id) {
|
||||||
|
Toast.show({
|
||||||
|
type: "error",
|
||||||
|
text1: "Gagal mengunggah bukti transfer",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiInvestmentUpdateInvoice({
|
||||||
|
id: id as string,
|
||||||
|
data: {
|
||||||
|
imageId: responseUploadImage?.data?.id,
|
||||||
|
},
|
||||||
|
status: "proses",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
Toast.show({
|
||||||
|
type: "success",
|
||||||
|
text1: "Berhasil mengunggah bukti transfer",
|
||||||
|
});
|
||||||
|
router.push(`/investment/${id}/(transaction-flow)/process`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<StackCustom>
|
||||||
|
<InformationBox text="Mohon transfer ke rekening dibawah" />
|
||||||
|
<BaseBox>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextCustom>Bank</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<Spacing height={10} />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextCustom>Nama Akun</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||||
|
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||||
|
<Grid.Col
|
||||||
|
span={8}
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextCustom size="xlarge" bold color="yellow">
|
||||||
|
{data?.MasterBank?.norek}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={4}
|
||||||
|
style={{
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyButton textToCopy={data?.MasterBank?.norek} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
|
||||||
|
<BaseBox>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom>Jumlah Transaksi</TextCustom>
|
||||||
|
|
||||||
|
<Spacing height={10} />
|
||||||
|
|
||||||
|
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||||
|
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||||
|
<Grid.Col
|
||||||
|
span={8}
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextCustom size="xlarge" bold color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(data?.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={4}
|
||||||
|
style={{
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyButton textToCopy={data?.nominal} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
|
||||||
|
<BaseBox>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom align="center">
|
||||||
|
Upload bukti transfer anda.
|
||||||
|
</TextCustom>
|
||||||
|
{image ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 10,
|
||||||
|
paddingInline: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextCustom bold align="center" truncate>
|
||||||
|
{image?.name}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<TextCustom align="center">
|
||||||
|
Tidak ada gambar yang diunggah
|
||||||
|
</TextCustom>
|
||||||
|
)}
|
||||||
|
<ButtonCenteredOnly
|
||||||
|
onPress={() => {
|
||||||
|
pickFile({
|
||||||
|
allowedType: "image",
|
||||||
|
setImageUri(file: any) {
|
||||||
|
setImage(file);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon="upload"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</ButtonCenteredOnly>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={!image || isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmitUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Saya Sudah Transfer
|
||||||
|
</ButtonCustom>
|
||||||
|
</StackCustom>
|
||||||
|
<Spacing />
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
screens/Invesment/ScreenMyHolding.tsx
Normal file
91
screens/Invesment/ScreenMyHolding.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BaseBox,
|
||||||
|
ProgressCustom,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom
|
||||||
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
export default function Investment_ScreenMyHolding() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiInvestmentGetAll({
|
||||||
|
category: "my-holding",
|
||||||
|
authorId: user?.id,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [user?.id],
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch my holding:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [user?.id]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BaseBox
|
||||||
|
paddingTop={7}
|
||||||
|
paddingBottom={7}
|
||||||
|
onPress={() =>
|
||||||
|
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
||||||
|
<TextCustom>Rp. {formatCurrencyDisplay(item?.nominal)}</TextCustom>
|
||||||
|
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
|
||||||
|
<ProgressCustom
|
||||||
|
label={`${item.progress}%`}
|
||||||
|
value={Number(item.progress)}
|
||||||
|
size="lg"
|
||||||
|
animated
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada investasi yang dimiliki",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -65,6 +65,8 @@ export default function Investment_ScreenTransaction() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePress = ({ id, status }: { id: string; status: string }) => {
|
const handlePress = ({ id, status }: { id: string; status: string }) => {
|
||||||
|
console.log("ID", id);
|
||||||
|
console.log("Status", status);
|
||||||
if (status === "menunggu") {
|
if (status === "menunggu") {
|
||||||
router.push(`/investment/${id}/(transaction-flow)/invoice`);
|
router.push(`/investment/${id}/(transaction-flow)/invoice`);
|
||||||
} else if (status === "proses") {
|
} else if (status === "proses") {
|
||||||
|
|||||||
@@ -238,13 +238,15 @@ export async function apiInvestmentCreateNews({
|
|||||||
export async function apiInvestmentGetNews({
|
export async function apiInvestmentGetNews({
|
||||||
id,
|
id,
|
||||||
category,
|
category,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
category: "all-news" | "one-news";
|
category: "all-news" | "one-news";
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/investment/${id}/news?category=${category}`
|
`/mobile/investment/${id}/news?category=${category}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -263,11 +265,13 @@ export async function apiInvestmentDeleteNews({ id }: { id: string }) {
|
|||||||
|
|
||||||
export async function apiInvestmentGetInvestorById({
|
export async function apiInvestmentGetInvestorById({
|
||||||
id,
|
id,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/investment/${id}/investor`);
|
const response = await apiConfig.get(`/mobile/investment/${id}/investor?page=${page}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user