Compare commits
7 Commits
loaddata/6
...
fixed-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 | |||
| 5c931b069c | |||
| b2be7be533 | |||
| 2705f96b01 | |||
| 38a6b424e8 |
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,56 +1,9 @@
|
|||||||
import {
|
import Donation_ScreenBeranda from "@/screens/Donation/ScreenBeranda";
|
||||||
FloatingButton,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
|
|
||||||
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationBeranda() {
|
export default function DonationBeranda() {
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetAll({
|
|
||||||
category: "beranda"
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Donation_ScreenBeranda />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/donation/create")} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">Belum ada donasi</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<Donation_BoxPublish data={item} key={index} id={item.id} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,148 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Donation_ScreenMyDonation from "@/screens/Donation/ScreenMyDonation";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
DummyLandscapeImage,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { Href, router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function DonationMyDonation() {
|
export default function DonationMyDonation() {
|
||||||
const { user } = useAuth();
|
return <Donation_ScreenMyDonation />;
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
if (!user?.id) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Load data gagal, user tidak ditemukan",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetAll({
|
|
||||||
category: "my-donation",
|
|
||||||
authorId: user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlerColor = (status: string) => {
|
|
||||||
if (status === "menunggu") {
|
|
||||||
return "orange";
|
|
||||||
} else if (status === "proses") {
|
|
||||||
return "white";
|
|
||||||
} else if (status === "berhasil") {
|
|
||||||
return "green";
|
|
||||||
} else if (status === "gagal") {
|
|
||||||
return "red";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = ({
|
|
||||||
invoiceId,
|
|
||||||
donationId,
|
|
||||||
status,
|
|
||||||
}: {
|
|
||||||
invoiceId: string;
|
|
||||||
donationId: string;
|
|
||||||
status: string;
|
|
||||||
}) => {
|
|
||||||
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
|
|
||||||
if (status === "menunggu") {
|
|
||||||
router.push(`${url}/invoice`);
|
|
||||||
} else if (status === "proses") {
|
|
||||||
router.push(`${url}/process`);
|
|
||||||
} else if (status === "berhasil") {
|
|
||||||
router.push(`${url}/success`);
|
|
||||||
} else if (status === "gagal") {
|
|
||||||
router.push(`${url}/failed`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewWrapper hideFooter>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada transaksi
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item, index) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingTop={7}
|
|
||||||
paddingBottom={7}
|
|
||||||
onPress={() => {
|
|
||||||
handlePress({
|
|
||||||
status: _.lowerCase(item.statusInvoice),
|
|
||||||
invoiceId: item.id,
|
|
||||||
donationId: item.donasiId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={5}>
|
|
||||||
<DummyLandscapeImage
|
|
||||||
height={100}
|
|
||||||
unClickPath
|
|
||||||
imageId={item.imageId}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={1}>
|
|
||||||
<View />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom truncate={2} bold>
|
|
||||||
{item.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(item.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<BadgeCustom
|
|
||||||
variant="light"
|
|
||||||
color={handlerColor(_.lowerCase(item.statusInvoice))}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{item.statusInvoice}
|
|
||||||
</BadgeCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Donation_ScreenStatus from "@/screens/Donation/ScreenStatus";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
|
||||||
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationStatus() {
|
export default function DonationStatus() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish",
|
|
||||||
);
|
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [activeCategory]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetByStatus({
|
|
||||||
authorId: user?.id as string,
|
|
||||||
status: activeCategory as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListData(null);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter headerComponent={scrollComponent}>
|
<Donation_ScreenStatus initialStatus={status || "publish"} />
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<Donasi_BoxStatus
|
|
||||||
key={index}
|
|
||||||
data={item}
|
|
||||||
status={activeCategory as string}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -31,7 +32,7 @@ export default function DonationEditNews() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [news])
|
}, [news]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -104,7 +105,21 @@ export default function DonationEditNews() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<ViewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!data?.title || !data?.deskripsi}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmitUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||||
<LandscapeFrameUploaded
|
<LandscapeFrameUploaded
|
||||||
@@ -148,15 +163,6 @@ export default function DonationEditNews() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
disabled={!data?.title || !data?.deskripsi}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmitUpdate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
@@ -53,7 +55,7 @@ export default function DonationAddNews() {
|
|||||||
text1: "Gagal menambah berita",
|
text1: "Gagal menambah berita",
|
||||||
});
|
});
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.show({
|
Toast.show({
|
||||||
@@ -70,7 +72,21 @@ export default function DonationAddNews() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!data.title || !data.deskripsi}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||||
<LandscapeFrameUploaded image={image?.uri} />
|
<LandscapeFrameUploaded image={image?.uri} />
|
||||||
@@ -116,17 +132,7 @@ export default function DonationAddNews() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
disabled={!data.title || !data.deskripsi}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
</NewWrapper>
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenListOfNews from "@/screens/Donation/ScreenListOfNews";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
|
||||||
import { formatChatTime } from "@/utils/formatChatTime";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationRecapOfNews() {
|
export default function DonationRecapOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
return <Donation_ScreenListOfNews donationId={id as string} />;
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetNewsById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Daftar Kabar",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada kabar
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom truncate bold>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom size="small">
|
|
||||||
{formatChatTime(item?.createdAt)}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconPlus />,
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/donation/${id}/(news)/add-news`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
console.log("PATH ", item.path);
|
|
||||||
router.navigate(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenRecapOfNews from "@/screens/Donation/ScreenRecapOfNews";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DotButton,
|
|
||||||
DrawerCustom,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
|
||||||
import { formatChatTime } from "@/utils/formatChatTime";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationRecapOfNews() {
|
export default function DonationRecapOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
return <Donation_ScreenRecapOfNews donationId={id as string} />;
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetNewsById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Rekap Kabar",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada kabar
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom truncate bold>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom size="small">
|
|
||||||
{formatChatTime(item?.createdAt)}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconPlus />,
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/donation/${id}/(news)/add-news`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
console.log("PATH ", item.path);
|
|
||||||
router.navigate(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
BaseBox,
|
BaseBox,
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
Grid,
|
Grid,
|
||||||
@@ -35,7 +36,7 @@ export default function DonationInvoice() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [invoiceId])
|
}, [invoiceId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -100,7 +101,22 @@ export default function DonationInvoice() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper>
|
<ViewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!image}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerUpdateInvoice();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<InformationBox
|
<InformationBox
|
||||||
text={`Mohon transfer donasi anda ke rekening dibawah`}
|
text={`Mohon transfer donasi anda ke rekening dibawah`}
|
||||||
@@ -204,16 +220,6 @@ export default function DonationInvoice() {
|
|||||||
</ButtonCenteredOnly>
|
</ButtonCenteredOnly>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
<ButtonCustom
|
|
||||||
disabled={!image}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerUpdateInvoice();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
DotButton,
|
DotButton,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { IconEdit, IconNews } from "@/components/_Icon";
|
import { IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
|
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
|
||||||
@@ -26,18 +27,19 @@ import {
|
|||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
export default function DonasiDetailStatus() {
|
export default function DonasiDetailStatus() {
|
||||||
const { id, status } = useLocalSearchParams();
|
const { id, status } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [data, setData] = useState<any>();
|
const [data, setData] = useState<any | null>(null);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -80,6 +82,17 @@ export default function DonasiDetailStatus() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefresh = useCallback(() => {
|
||||||
|
try {
|
||||||
|
setRefreshing(true);
|
||||||
|
onLoadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error refresh");
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -94,31 +107,50 @@ export default function DonasiDetailStatus() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
<Donation_ComponentBoxDetailData
|
refreshControl={
|
||||||
sisaHari={value.sisa}
|
<RefreshControl
|
||||||
reminder={value.reminder}
|
refreshing={refreshing}
|
||||||
data={data}
|
onRefresh={onRefresh}
|
||||||
bottomSection={
|
tintColor={MainColor.yellow}
|
||||||
status === "publish" && (
|
colors={[MainColor.yellow]}
|
||||||
<Donation_ProgressSection
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!data ? (
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Donation_ComponentBoxDetailData
|
||||||
|
sisaHari={value.sisa}
|
||||||
|
reminder={value.reminder}
|
||||||
|
data={data}
|
||||||
|
showSisaHari={status === "publish" ? true : false}
|
||||||
|
bottomSection={
|
||||||
|
status === "publish" && (
|
||||||
|
<Donation_ProgressSection
|
||||||
|
id={id as string}
|
||||||
|
progres={Number(data?.progres) || 0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Donation_ComponentStoryFunrising
|
||||||
|
id={id as string}
|
||||||
|
dataStory={data?.CeritaDonasi}
|
||||||
|
/>
|
||||||
|
<Spacing />
|
||||||
|
{data && (
|
||||||
|
<Donation_ButtonStatusSection
|
||||||
id={id as string}
|
id={id as string}
|
||||||
progres={Number(data?.progres) || 0}
|
status={status as string}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
/>
|
<Spacing />
|
||||||
<Donation_ComponentStoryFunrising
|
</>
|
||||||
id={id as string}
|
)}
|
||||||
dataStory={data?.CeritaDonasi}
|
</NewWrapper>
|
||||||
/>
|
|
||||||
<Spacing />
|
|
||||||
<Donation_ButtonStatusSection
|
|
||||||
id={id as string}
|
|
||||||
status={status as string}
|
|
||||||
/>
|
|
||||||
<Spacing />
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
isVisible={openDrawer}
|
isVisible={openDrawer}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +63,7 @@ export default function DonationEdit() {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
onLoadList();
|
onLoadList();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -79,7 +82,6 @@ export default function DonationEdit() {
|
|||||||
imageId: response.data.imageId,
|
imageId: response.data.imageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
}
|
}
|
||||||
@@ -182,10 +184,24 @@ export default function DonationEdit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmitUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
||||||
{!data || loadList ? (
|
{!data || loadList ? (
|
||||||
<LoaderCustom />
|
<ListSkeletonComponent />
|
||||||
) : (
|
) : (
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
@@ -260,17 +276,9 @@ export default function DonationEdit() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmitUpdate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenFundDisbursement from "@/screens/Donation/ScreenFundDisbursement";
|
||||||
BaseBox,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
Grid,
|
|
||||||
InformationBox,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import {
|
|
||||||
apiDonationDisbursementOfFundsListById,
|
|
||||||
apiDonationGetOne,
|
|
||||||
} from "@/service/api-client/api-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationFundDisbursement() {
|
export default function DonationFundDisbursement() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
|
|
||||||
const [data, setData] = useState({
|
return <Donation_ScreenFundDisbursement donationId={id as string} />;
|
||||||
totalPencairan: 0,
|
|
||||||
akumulasiPencairan: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
|
|
||||||
const responseData = await apiDonationGetOne({
|
|
||||||
id: id as string,
|
|
||||||
category: "permanent",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseData.success) {
|
|
||||||
setData({
|
|
||||||
totalPencairan: responseData.data.totalPencairan,
|
|
||||||
akumulasiPencairan: responseData.data.akumulasiPencairan,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseList = await apiDonationDisbursementOfFundsListById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseList.success) {
|
|
||||||
setListData(responseList.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
|
|
||||||
<BaseBox>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom size="small">Total Pencairan Dana</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
{data?.akumulasiPencairan} kali
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<StackCustom>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>{item?.title}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom>{dayjs(item?.createdAt).format("DD MMM YYYY")}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<TextCustom>{item?.deskripsi}</TextCustom>
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
onPress={() => {
|
|
||||||
router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`);
|
|
||||||
}}
|
|
||||||
icon="file-text"
|
|
||||||
>
|
|
||||||
Bukti Transaksi
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
DotButton,
|
DotButton,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { IconNews } from "@/components/_Icon";
|
import { IconNews } from "@/components/_Icon";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
|
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
|
||||||
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
|
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
|
||||||
@@ -34,7 +36,7 @@ export default function DonasiDetailBeranda() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -75,10 +77,10 @@ export default function DonasiDetailBeranda() {
|
|||||||
<>
|
<>
|
||||||
<BoxButtonOnFooter>
|
<BoxButtonOnFooter>
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={value?.reminder}
|
disabled={value?.reminder || !data}
|
||||||
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
|
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
|
||||||
>
|
>
|
||||||
{value?.reminder ? "Waktu berakhir" : "Donasi"}
|
{!data ? "Loading..." : value?.reminder ? "Waktu berakhir" : "Donasi"}
|
||||||
</ButtonCustom>
|
</ButtonCustom>
|
||||||
</BoxButtonOnFooter>
|
</BoxButtonOnFooter>
|
||||||
</>
|
</>
|
||||||
@@ -96,21 +98,30 @@ export default function DonasiDetailBeranda() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper footerComponent={buttonSection}>
|
<NewWrapper footerComponent={buttonSection}>
|
||||||
<StackCustom>
|
{!data ? (
|
||||||
<Donation_ComponentBoxDetailData
|
<CustomSkeleton height={400} />
|
||||||
sisaHari={value.sisa}
|
) : (
|
||||||
reminder={value.reminder}
|
<StackCustom>
|
||||||
data={data}
|
<Donation_ComponentBoxDetailData
|
||||||
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
|
sisaHari={value.sisa}
|
||||||
/>
|
reminder={value.reminder}
|
||||||
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
data={data}
|
||||||
<Donation_ComponentStoryFunrising
|
bottomSection={
|
||||||
id={id as string}
|
<Donation_ProgressSection
|
||||||
dataStory={data?.CeritaDonasi}
|
id={id as string}
|
||||||
/>
|
progres={Number(data?.progres) || 0}
|
||||||
</StackCustom>
|
/>
|
||||||
</ViewWrapper>
|
}
|
||||||
|
/>
|
||||||
|
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
||||||
|
<Donation_ComponentStoryFunrising
|
||||||
|
id={id as string}
|
||||||
|
dataStory={data?.CeritaDonasi}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
)}
|
||||||
|
</NewWrapper>
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
isVisible={openDrawer}
|
isVisible={openDrawer}
|
||||||
|
|||||||
@@ -1,94 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenListOfDonatur from "@/screens/Donation/ScreenListOfDonatur";
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { apiAdminDonationListOfDonaturById } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { FontAwesome6 } from "@expo/vector-icons";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function Donation_ListOfDonatur() {
|
export default function DonationListOfDonatur() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
return <Donation_ScreenListOfDonatur donationId={id as string} />;
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationListOfDonaturById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Belum ada donatur
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col
|
|
||||||
span={3}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
|
||||||
<FontAwesome6
|
|
||||||
name="face-smile-wink"
|
|
||||||
size={50}
|
|
||||||
style={{ color: MainColor.yellow }}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={9}>
|
|
||||||
<TextCustom bold size="large">
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
<Spacing/>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
|
|
||||||
<TextCustom bold size="large" color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>
|
|
||||||
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
|
|
||||||
</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -8,8 +9,8 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
@@ -112,7 +113,23 @@ export default function DonationCreateStory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<>
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
|
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
|
||||||
<TextAreaCustom
|
<TextAreaCustom
|
||||||
@@ -166,18 +183,8 @@ export default function DonationCreateStory() {
|
|||||||
value={data.rekening}
|
value={data.rekening}
|
||||||
onChangeText={(value) => setData({ ...data, rekening: value })}
|
onChangeText={(value) => setData({ ...data, rekening: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -8,8 +9,8 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { apiDonationCreate } from "@/service/api-client/api-donation";
|
import { apiDonationCreate } from "@/service/api-client/api-donation";
|
||||||
import { apiMasterDonation } from "@/service/api-client/api-master";
|
import { apiMasterDonation } from "@/service/api-client/api-master";
|
||||||
@@ -43,7 +44,7 @@ export default function DonationCreate() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadList();
|
onLoadList();
|
||||||
}, [])
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadList = async () => {
|
const onLoadList = async () => {
|
||||||
@@ -125,7 +126,24 @@ export default function DonationCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<>
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Selanjutnya
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
||||||
|
|
||||||
@@ -201,20 +219,8 @@ export default function DonationCreate() {
|
|||||||
onChange={(value: any) => setData({ ...data, durasiId: value })}
|
onChange={(value: any) => setData({ ...data, durasiId: value })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Selanjutnya
|
|
||||||
</ButtonCustom>
|
|
||||||
<Spacing />
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { StackCustom, ViewWrapper } from "@/components";
|
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
@@ -29,14 +29,14 @@ export default function Application() {
|
|||||||
checkVersion();
|
checkVersion();
|
||||||
userData(token as string);
|
userData(token as string);
|
||||||
syncUnreadCount();
|
syncUnreadCount();
|
||||||
}, [user?.id, token])
|
}, [user?.id, token]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData() {
|
async function onLoadData() {
|
||||||
const response = await apiUser(user?.id as string);
|
const response = await apiUser(user?.id as string);
|
||||||
console.log(
|
console.log(
|
||||||
"[Profile ID]>>",
|
"[Profile ID]>>",
|
||||||
JSON.stringify(response?.data?.Profile?.id, null, 2)
|
JSON.stringify(response?.data?.Profile?.id, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
@@ -61,12 +61,29 @@ export default function Application() {
|
|||||||
|
|
||||||
if (data && data?.active === false) {
|
if (data && data?.active === false) {
|
||||||
console.log("User is not active");
|
console.log("User is not active");
|
||||||
return <Redirect href={`/waiting-room`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/waiting-room`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data?.Profile === null) {
|
if (data && data?.Profile === null) {
|
||||||
console.log("Profile is null");
|
console.log("Profile is null");
|
||||||
return <Redirect href={`/profile/create`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/profile/create`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data?.masterUserRoleId !== "1") {
|
||||||
|
console.log("User is not admin");
|
||||||
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/admin/dashboard`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,7 +106,12 @@ export default function Application() {
|
|||||||
/>
|
/>
|
||||||
<ViewWrapper
|
<ViewWrapper
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
footerComponent={
|
footerComponent={
|
||||||
<TabSection
|
<TabSection
|
||||||
|
|||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export default function InvestmentDetailStatus() {
|
|||||||
updateCountDown();
|
updateCountDown();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
const updateCountDown = () => {
|
const updateCountDown = () => {
|
||||||
const countDown = countDownAndCondition({
|
const countDown = countDownAndCondition({
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export default function InvestmentDetail() {
|
|||||||
updateCountDown();
|
updateCountDown();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
const updateCountDown = () => {
|
const updateCountDown = () => {
|
||||||
const countDown = countDownAndCondition({
|
const countDown = countDownAndCondition({
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
||||||
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
||||||
|
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
|
||||||
|
import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
import {
|
import {
|
||||||
ICON_SIZE_MEDIUM,
|
ICON_SIZE_MEDIUM,
|
||||||
@@ -20,6 +22,10 @@ import {
|
|||||||
adminListMenu,
|
adminListMenu,
|
||||||
superAdminListMenu,
|
superAdminListMenu,
|
||||||
} from "@/screens/Admin/listPageAdmin";
|
} from "@/screens/Admin/listPageAdmin";
|
||||||
|
import {
|
||||||
|
adminListMenu_V2,
|
||||||
|
superAdminListMenu_V2,
|
||||||
|
} from "@/screens/Admin/listPageAdmin_V2";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import { GStyles } from "@/styles/global-styles";
|
||||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
@@ -148,6 +154,24 @@ export default function AdminLayout() {
|
|||||||
}
|
}
|
||||||
onClose={() => setOpenDrawerNavbar(false)}
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V2
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V3
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</DrawerAdmin>
|
</DrawerAdmin>
|
||||||
|
|
||||||
@@ -198,7 +222,7 @@ export default function AdminLayout() {
|
|||||||
// size={ICON_SIZE_SMALL}
|
// size={ICON_SIZE_SMALL}
|
||||||
// color={MainColor.white}
|
// color={MainColor.white}
|
||||||
// />
|
// />
|
||||||
<AdminNotificationBell/>
|
<AdminNotificationBell />
|
||||||
),
|
),
|
||||||
path: "/admin/notification",
|
path: "/admin/notification",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,139 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenUserAccess } from "@/screens/Admin/User-Access/ScreenUserAccess";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
Divider,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
|
||||||
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function AdminUserAccess() {
|
export default function AdminUserAccess() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenUserAccess />;
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiAdminUserAccessGetAll({
|
|
||||||
search: search,
|
|
||||||
category: "only-user",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR LOAD DATA]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari User"
|
|
||||||
onChangeText={(text) => setSearch(text)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="User Access"
|
|
||||||
rightComponent={rightComponent()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<TextCustom align="center" bold>
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={<TextCustom bold>Username</TextCustom>}
|
|
||||||
component3={
|
|
||||||
<TextCustom align="center" bold>
|
|
||||||
Status Akses
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{_.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray" size={"small"}>
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<GridViewCustomSpan
|
|
||||||
key={index}
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<Ionicons
|
|
||||||
onPress={() =>
|
|
||||||
router.push(`/admin/user-access/${item?.id}`)
|
|
||||||
}
|
|
||||||
name="open"
|
|
||||||
size={ICON_SIZE_XLARGE}
|
|
||||||
color={MainColor.yellow}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
// <ButtonCustom
|
|
||||||
// onPress={() =>
|
|
||||||
// router.push(`/admin/user-access/${item?.id}`)
|
|
||||||
// }
|
|
||||||
// >
|
|
||||||
// Detail
|
|
||||||
// </ButtonCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
{item?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<CenterCustom>
|
|
||||||
{item?.active ? (
|
|
||||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
|
||||||
) : (
|
|
||||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
|
||||||
)}
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
style3={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
276
components/Drawer/NavbarMenu.back.tsx
Normal file
276
components/Drawer/NavbarMenu.back.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Set activeLink saat pathname berubah
|
||||||
|
useEffect(() => {
|
||||||
|
if (normalizedPathname) {
|
||||||
|
setActiveLink(normalizedPathname);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
// flex: 1,
|
||||||
|
// backgroundColor: MainColor.black,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10, // Opsional: tambahkan padding
|
||||||
|
}}
|
||||||
|
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
activeLink={activeLink}
|
||||||
|
setActiveLink={setActiveLink}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
activeLink,
|
||||||
|
setActiveLink,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem;
|
||||||
|
onClose?: () => void;
|
||||||
|
activeLink: string | null;
|
||||||
|
setActiveLink: (link: string | null) => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const isActive = activeLink === item.link;
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
React.useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.parentText}>{item.label}</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
// {
|
||||||
|
// backgroundColor: "red",
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 40],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.links.map((subItem, index) => {
|
||||||
|
const isSubActive = activeLink === subItem.link;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, isSubActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(subItem.link);
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={isSubActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
isSubActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(item.link || null);
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -28,7 +28,7 @@ interface NavbarMenuProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [activeLink, setActiveLink] = useState<string | null>(null);
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (normalizedPathname) {
|
if (normalizedPathname) {
|
||||||
setActiveLink(normalizedPathname);
|
setActiveLink(normalizedPathname);
|
||||||
|
|
||||||
|
// Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
|
||||||
|
for (const item of items) {
|
||||||
|
// Cocokkan dengan link langsung
|
||||||
|
if (item.link && normalizedPathname.startsWith(item.link)) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cocokkan dengan submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
|
||||||
|
if (matchingSubItem) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [normalizedPathname]);
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
// Toggle dropdown
|
// Toggle dropdown
|
||||||
const toggleOpen = (label: string) => {
|
const toggleOpen = (label: string) => {
|
||||||
setOpenKeys((prev) =>
|
setOpenKeys((prev) =>
|
||||||
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,35 +125,71 @@ function MenuItem({
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggleOpen: () => void;
|
toggleOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
// Cek apakah menu ini atau submenu-nya yang aktif
|
||||||
const isActive = activeLink === item.link;
|
const isActive = activeLink === item.link;
|
||||||
|
|
||||||
|
// Cek apakah path saat ini cocok dengan salah satu submenu
|
||||||
|
const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
|
||||||
|
|
||||||
|
// Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
|
||||||
|
const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return activeLink.startsWith(linkPath + "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
|
||||||
|
const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
|
||||||
|
|
||||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
// Animasi saat isOpen berubah
|
// Animasi saat isOpen berubah
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
|
||||||
|
const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
|
||||||
|
|
||||||
Animated.timing(animatedHeight, {
|
Animated.timing(animatedHeight, {
|
||||||
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
duration: 200,
|
duration: 200,
|
||||||
useNativeDriver: false,
|
useNativeDriver: false,
|
||||||
}).start();
|
}).start();
|
||||||
}, [isOpen, item.links, animatedHeight]);
|
|
||||||
|
// Jika perlu membuka dropdown otomatis, panggil toggleOpen
|
||||||
|
if (shouldAutoOpen) {
|
||||||
|
toggleOpen();
|
||||||
|
}
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
|
||||||
|
|
||||||
// Jika ada submenu
|
// Jika ada submenu
|
||||||
if (item.links && item.links.length > 0) {
|
if (item.links && item.links.length > 0) {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{/* Parent Item */}
|
{/* Parent Item */}
|
||||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isMenuActive && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.parentText}>{item.label}</Text>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isMenuActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
parentText: {
|
parentText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V2 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V2[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// Check jika ada submenu yang match dengan current path
|
||||||
|
const hasActiveSubmenu = item.links.some((subItem) => {
|
||||||
|
return checkPathMatch(subItem.link, subItem.detailPattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasActiveSubmenu) {
|
||||||
|
newOpenKeys.push(item.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V2;
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
console.log('🔍 Pattern Match Check:', {
|
||||||
|
currentPath,
|
||||||
|
detailPattern,
|
||||||
|
regex: patternRegex.toString(),
|
||||||
|
isMatch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
const hasActiveSubmenu =
|
||||||
|
item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// PRE-CALCULATE semua active states untuk submenu
|
||||||
|
const submenuActiveStates = item.links.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
hasActiveSubmenu && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
hasActiveSubmenu && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
dashboardLink: subItem.link,
|
||||||
|
dashboardLength: pathLength,
|
||||||
|
otherLabel: other.subItem.label,
|
||||||
|
otherLink: other.subItem.link,
|
||||||
|
otherPattern: other.subItem.detailPattern,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
otherIsActive: other.isActive,
|
||||||
|
isOtherLonger,
|
||||||
|
willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
if (isSubActive && other.isActive) {
|
||||||
|
console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
current: subItem.label,
|
||||||
|
currentPath: subItem.link,
|
||||||
|
currentLength: pathLength,
|
||||||
|
other: other.subItem.label,
|
||||||
|
otherPath: other.subItem.link,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
isOtherLonger,
|
||||||
|
shouldDisableCurrent: isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
if (isSubActive) {
|
||||||
|
console.log('✅ Active check:', {
|
||||||
|
label: subItem.label,
|
||||||
|
link: subItem.link,
|
||||||
|
isSubActive,
|
||||||
|
hasMoreSpecificMatch,
|
||||||
|
finalIsActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, finalIsActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={finalIsActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
finalIsActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V3 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate all potential matches for conflict resolution
|
||||||
|
const allMatches = items.flatMap(item => {
|
||||||
|
if (!item.links || item.links.length === 0) return [];
|
||||||
|
|
||||||
|
return item.links
|
||||||
|
.filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
|
||||||
|
.map(subItem => ({
|
||||||
|
parentLabel: item.label,
|
||||||
|
subItem,
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the most specific match for each parent
|
||||||
|
const uniqueParents = new Map<string, { parentLabel: string, longestPathLength: number }>();
|
||||||
|
|
||||||
|
allMatches.forEach(match => {
|
||||||
|
const existing = uniqueParents.get(match.parentLabel);
|
||||||
|
if (!existing || match.pathLength > existing.longestPathLength) {
|
||||||
|
uniqueParents.set(match.parentLabel, {
|
||||||
|
parentLabel: match.parentLabel,
|
||||||
|
longestPathLength: match.pathLength
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add only the parents with the most specific matches
|
||||||
|
newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
|
||||||
|
|
||||||
|
// Additionally, if no specific submenu match was found but the current path
|
||||||
|
// starts with one of the parent menu links, add that parent
|
||||||
|
if (newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPageGlobal = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Only open parent menu if the current path is a detail page of the most relevant parent
|
||||||
|
if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V3;
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
// if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
// console.log('🔍 Pattern Match Check:', {
|
||||||
|
// currentPath,
|
||||||
|
// detailPattern,
|
||||||
|
// regex: patternRegex.toString(),
|
||||||
|
// isMatch
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasCommonWord = segments.some(segment =>
|
||||||
|
commonWords.includes(segment.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hanya anggap sebagai detail page jika mengandung commonWords
|
||||||
|
return hasCommonWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = currentPath.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Calculate all submenu active states for conflict resolution
|
||||||
|
const submenuActiveStates = item.links?.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Determine if any submenu is active considering conflicts
|
||||||
|
const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
|
||||||
|
if (!isSubActive) return false;
|
||||||
|
|
||||||
|
// Check if there's a more specific match elsewhere
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
return other.isActive && other.pathLength > pathLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isSubActive && !hasMoreSpecificMatch;
|
||||||
|
}) || false;
|
||||||
|
|
||||||
|
// For parent menu detection, if current path contains common words,
|
||||||
|
// check if this parent menu's link is a prefix of the current path
|
||||||
|
const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
|
||||||
|
|
||||||
|
// Determine if this is the most relevant parent menu for the current path
|
||||||
|
const isMostRelevantParent = isParentOfDetailPage && (() => {
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
let mostRelevantParent = null;
|
||||||
|
|
||||||
|
// Find the parent with the longest matching prefix
|
||||||
|
items.forEach(parentItem => {
|
||||||
|
if (parentItem.links && parentItem.links.length > 0) {
|
||||||
|
parentItem.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
mostRelevantParent = parentItem.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mostRelevantParent === item.label;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
|
||||||
|
const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
|
||||||
|
|
||||||
|
// NEW LOGIC: If user is on a detail page that belongs to this parent menu,
|
||||||
|
// activate only the parent menu (open dropdown) without activating any submenu
|
||||||
|
const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
|
||||||
|
item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return currentPath.startsWith(linkPath + "/");
|
||||||
|
}) &&
|
||||||
|
!isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
|
||||||
|
const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
|
||||||
|
// then it should not be highlighted even if it would normally be the most relevant
|
||||||
|
const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
|
||||||
|
|
||||||
|
// NEW LOGIC: If this is an unlisted page, no menu should be highlighted
|
||||||
|
const isUnlistedPageAndNotRelevant = isUnlistedPage;
|
||||||
|
|
||||||
|
// FINAL LOGIC: Only activate this menu if:
|
||||||
|
// 1. It's the exact match for current path, OR
|
||||||
|
// 2. It's the most relevant parent, OR
|
||||||
|
// 3. It's a detail page of this menu
|
||||||
|
// But NOT if we're on a detail page and this isn't the relevant parent
|
||||||
|
// And NOT if this is an unlisted page
|
||||||
|
const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isActuallyRelevant && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isActuallyRelevant && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
// if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
// console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
// dashboardLink: subItem.link,
|
||||||
|
// dashboardLength: pathLength,
|
||||||
|
// otherLabel: other.subItem.label,
|
||||||
|
// otherLink: other.subItem.link,
|
||||||
|
// otherPattern: other.subItem.detailPattern,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// otherIsActive: other.isActive,
|
||||||
|
// isOtherLonger,
|
||||||
|
// willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
// if (isSubActive && other.isActive) {
|
||||||
|
// console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
// current: subItem.label,
|
||||||
|
// currentPath: subItem.link,
|
||||||
|
// currentLength: pathLength,
|
||||||
|
// other: other.subItem.label,
|
||||||
|
// otherPath: other.subItem.link,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// isOtherLonger,
|
||||||
|
// shouldDisableCurrent: isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
|
||||||
|
// Also don't highlight if this is an unlisted page
|
||||||
|
const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
// if (isSubActive) {
|
||||||
|
// console.log('✅ Active check:', {
|
||||||
|
// label: subItem.label,
|
||||||
|
// link: subItem.link,
|
||||||
|
// isSubActive,
|
||||||
|
// hasMoreSpecificMatch,
|
||||||
|
// finalIsActive,
|
||||||
|
// shouldHighlight,
|
||||||
|
// isOnDetailPage,
|
||||||
|
// isUnlistedPage
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={shouldHighlight ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
shouldHighlight && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import BaseBox from "@/components/Box/BaseBox";
|
||||||
|
import TextCustom from "@/components/Text/TextCustom";
|
||||||
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
|
import { StyleProp, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onPress?: () => void;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminBasicBox({ children, onPress, style }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox onPress={onPress} style={style}>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import BaseBox from "@/components/Box/BaseBox";
|
import BaseBox from "@/components/Box/BaseBox";
|
||||||
import Grid from "@/components/Grid/GridCustom";
|
import Grid from "@/components/Grid/GridCustom";
|
||||||
import TextCustom from "@/components/Text/TextCustom";
|
import TextCustom from "@/components/Text/TextCustom";
|
||||||
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
|
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
export default function AdminComp_BoxTitle({
|
export default function AdminComp_BoxTitle({
|
||||||
title,
|
title,
|
||||||
@@ -12,13 +14,33 @@ export default function AdminComp_BoxTitle({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseBox
|
{/* <BaseBox
|
||||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
||||||
paddingTop={5}
|
paddingTop={5}
|
||||||
paddingBottom={5}
|
paddingBottom={5}
|
||||||
|
backgroundColor={AccentColor.blue}
|
||||||
|
> */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: AccentColor.darkblue,
|
||||||
|
borderColor: AccentColor.blue,
|
||||||
|
|
||||||
|
padding: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Grid>
|
<Grid
|
||||||
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
|
// containerStyle={{
|
||||||
|
// bottom: 0,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<Grid.Col
|
||||||
|
span={rightComponent ? 6 : 12}
|
||||||
|
style={{ justifyContent: "center" }}
|
||||||
|
>
|
||||||
<TextCustom
|
<TextCustom
|
||||||
// style={{ alignSelf: "center" }}
|
// style={{ alignSelf: "center" }}
|
||||||
bold
|
bold
|
||||||
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</BaseBox>
|
</View>
|
||||||
|
{/* </BaseBox> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
export default function BasicWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
|
|||||||
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
||||||
import GridComponentView from "./_ShareComponent/GridSectionView";
|
import GridComponentView from "./_ShareComponent/GridSectionView";
|
||||||
import NewWrapper from "./_ShareComponent/NewWrapper";
|
import NewWrapper from "./_ShareComponent/NewWrapper";
|
||||||
|
import BasicWrapper from "./_ShareComponent/BasicWrapper";
|
||||||
// Progress
|
// Progress
|
||||||
import ProgressCustom from "./Progress/ProgressCustom";
|
import ProgressCustom from "./Progress/ProgressCustom";
|
||||||
// Loader
|
// Loader
|
||||||
@@ -121,6 +122,7 @@ export {
|
|||||||
GridComponentView,
|
GridComponentView,
|
||||||
Spacing,
|
Spacing,
|
||||||
NewWrapper,
|
NewWrapper,
|
||||||
|
BasicWrapper,
|
||||||
// Stack
|
// Stack
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TabBarBackground,
|
TabBarBackground,
|
||||||
|
|||||||
177
docs/admin-folder-structure.md
Normal file
177
docs/admin-folder-structure.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Struktur Folder Admin Aplikasi HIPMI Mobile
|
||||||
|
|
||||||
|
Dokumen ini menjelaskan struktur folder dan file untuk bagian admin dari aplikasi HIPMI Mobile yang terletak di `app/(application)/admin`.
|
||||||
|
|
||||||
|
## File dan Folder Tingkat Atas
|
||||||
|
|
||||||
|
### Folder
|
||||||
|
- `app-information` - Manajemen informasi aplikasi
|
||||||
|
- `collaboration` - Manajemen modul kolaborasi
|
||||||
|
- `donation` - Manajemen modul donasi
|
||||||
|
- `event` - Manajemen modul acara
|
||||||
|
- `forum` - Manajemen modul forum
|
||||||
|
- `investment` - Manajemen modul investasi
|
||||||
|
- `job` - Manajemen modul lowongan kerja
|
||||||
|
- `notification` - Manajemen notifikasi
|
||||||
|
- `super-admin` - Fungsi super admin
|
||||||
|
- `user-access` - Manajemen akses pengguna
|
||||||
|
- `voting` - Manajemen modul voting
|
||||||
|
|
||||||
|
### File
|
||||||
|
- `_layout.tsx` - Komponen tata letak untuk bagian admin
|
||||||
|
- `dashboard.tsx` - Tampilan dasbor admin
|
||||||
|
- `maps.tsx` - Fungsionalitas peta untuk admin
|
||||||
|
|
||||||
|
## Struktur Folder Terperinci
|
||||||
|
|
||||||
|
### app-information/
|
||||||
|
```
|
||||||
|
app-information/
|
||||||
|
├── business-field/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ ├── bidang-update.tsx
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── sub-bidang-update.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
├── information-bank/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
├── sticker/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### collaboration/
|
||||||
|
```
|
||||||
|
collaboration/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status].tsx
|
||||||
|
│ ├── group.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── group.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── publish.tsx
|
||||||
|
└── reject.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### donation/
|
||||||
|
```
|
||||||
|
donation/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── transaction-detail.tsx
|
||||||
|
│ ├── detail-disbursement-of-funds.tsx
|
||||||
|
│ ├── disbursement-of-funds.tsx
|
||||||
|
│ ├── list-disbursement-of-funds.tsx
|
||||||
|
│ ├── list-of-donatur.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── category-create.tsx
|
||||||
|
├── category-update.tsx
|
||||||
|
├── category.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### event/
|
||||||
|
```
|
||||||
|
event/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ ├── list-of-participants.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── type-create.tsx
|
||||||
|
├── type-of-event.tsx
|
||||||
|
└── type-update.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### forum/
|
||||||
|
```
|
||||||
|
forum/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── index.tsx
|
||||||
|
│ ├── list-comment.tsx
|
||||||
|
│ ├── list-report-comment.tsx
|
||||||
|
│ └── list-report-posting.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── posting.tsx
|
||||||
|
├── report-comment.tsx
|
||||||
|
└── report-posting.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### investment/
|
||||||
|
```
|
||||||
|
investment/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── transaction-detail.tsx
|
||||||
|
│ ├── list-of-investor.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### job/
|
||||||
|
```
|
||||||
|
job/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### notification/
|
||||||
|
```
|
||||||
|
notification/
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### super-admin/
|
||||||
|
```
|
||||||
|
super-admin/
|
||||||
|
├── [id]/
|
||||||
|
│ └── index.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### user-access/
|
||||||
|
```
|
||||||
|
user-access/
|
||||||
|
├── [id]/
|
||||||
|
│ └── index.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### voting/
|
||||||
|
```
|
||||||
|
voting/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── history.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rute Dinamis
|
||||||
|
|
||||||
|
Bagian admin menggunakan rute dinamis yang ditunjukkan dengan kurung siku `[ ]`:
|
||||||
|
- `[id]` - Rute dinamis untuk ID item tertentu
|
||||||
|
- `[status]` - Rute dinamis untuk tampilan berdasarkan status
|
||||||
|
|
||||||
|
Ini memungkinkan routing yang fleksibel berdasarkan parameter tertentu seperti ID item atau status.
|
||||||
@@ -1,8 +1,36 @@
|
|||||||
<!-- ===================== Start Penerapan Pagination ===================== -->
|
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
|
||||||
|
|
||||||
File utama: screens/Invesment/ScreenTransaction.tsx
|
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
|
||||||
Function fecth: apiInvestmentGetInvoice
|
Folder tujuan: screens/Donation
|
||||||
File function fetch: service/api-client/api-investment.ts
|
Nama file utama: ScreenFundDisbursement.tsx
|
||||||
|
|
||||||
|
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenFundDisbursement" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||||
|
Selanjutnya terapkan pagination pada file "Nama file utama"
|
||||||
|
|
||||||
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
|
File function fetch: service/api-client/api-donation.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/Donation/ScreenListOfNews.tsx
|
||||||
|
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan Pagination ` ===================== -->
|
||||||
|
|
||||||
|
<!-- ===================== Start Penerapan NewWrapper & Pagination ===================== -->
|
||||||
|
File utama: screens/Donation/ScreenFundDisbursement.tsx
|
||||||
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
|
File function fetch: service/api-client/api-donation.ts
|
||||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
Terapkan pagination pada file "File utama"
|
Terapkan pagination pada file "File utama"
|
||||||
@@ -15,21 +43,41 @@ 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 -->
|
|
||||||
File refrensi: screens/Event/ScreenStatus.tsx
|
|
||||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
|
||||||
|
|
||||||
<!-- ===================== End Penerapan Pagination ===================== -->
|
<!-- Additinal prompt -->
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
|
||||||
|
|
||||||
<!-- Start Penerapan NewWrapper -->
|
<!-- Start Penerapan NewWrapper -->
|
||||||
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
|
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
||||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
|
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||||
<!-- -->
|
<!-- End Penerapan NewWrapper -->
|
||||||
|
|
||||||
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
|
<!-- Start Random Prompt -->
|
||||||
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
|
|
||||||
jika tidak maka terapkan sesuai dengan logika yang sudah ada
|
|
||||||
|
|
||||||
Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
|
|
||||||
Ini adalah halaman yang memiliki fungsi pagination , saya membuat data dummy dimana menghasilkan data urut 1-9, saya mencoba memuat halaman setiap page nya 4 saja untuk percobaan.
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar
|
||||||
Saat awal muncul komponent box dengan data 9 - 6, kemudian saya hapus data ke 8 . lalu saya coba scroll ke bawah seharusnya angka akan tetap urut 9, 7, 6, 5, 4 ... 1. Tapi dalam case ini setelah 8 di hapus kemudian saya scroll box ke 5 tidak muncul saat di scroll. Apakah anda mengerti maksud saya ?
|
<!-- End Random Prompt -->
|
||||||
|
|
||||||
|
<!-- START Prompt Admin Refactoring -->
|
||||||
|
File source: app/(application)/admin/user-access/index.tsx
|
||||||
|
Folder tujuan: screens/Admin/User-Access
|
||||||
|
Nama file utama: ScreenUserAccess.tsx
|
||||||
|
Nama function utama: Admin_ScreenUserAccess
|
||||||
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
Function fecth: apiAdminUserAccessGetAll
|
||||||
|
File function fetch: service/api-admin/api-admin-user-access.ts
|
||||||
|
|
||||||
|
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Terapkan pagination pada file "Nama file utama"
|
||||||
|
|
||||||
|
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.
|
||||||
|
<!-- END Prompt Admin Refactoring -->
|
||||||
123
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
123
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
CenterCustom,
|
||||||
|
Grid,
|
||||||
|
SearchInput,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom
|
||||||
|
} from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import {
|
||||||
|
PAGINATION_DEFAULT_TAKE
|
||||||
|
} from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
export function Admin_ScreenUserAccess() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
return await apiAdminUserAccessGetAll({
|
||||||
|
search: searchQuery || "",
|
||||||
|
category: "only-user",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
// Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [],
|
||||||
|
onError: (error) => {
|
||||||
|
console.log("Error fetching data user access", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Tidak ada data pengguna",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rightComponent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchInput
|
||||||
|
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||||
|
placeholder="Cari User"
|
||||||
|
onChangeText={(text) => setSearch(text)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<AdminBasicBox
|
||||||
|
key={index}
|
||||||
|
onPress={() => router.push(`/admin/user-access/${item?.id}`)}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
{item?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size={"small"} bold truncate color="gray">
|
||||||
|
{item?.nomor || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<CenterCustom>
|
||||||
|
{item?.active ? (
|
||||||
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</CenterCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</AdminBasicBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
headerComponent={
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title="User Access"
|
||||||
|
rightComponent={rightComponent()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
renderItem={renderItem}
|
||||||
|
listData={pagination.listData}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
461
screens/Admin/listPageAdmin_V2.tsx
Normal file
461
screens/Admin/listPageAdmin_V2.tsx
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2";
|
||||||
|
|
||||||
|
|
||||||
|
export { adminListMenu_V2, superAdminListMenu_V2 }
|
||||||
|
|
||||||
|
const adminListMenu_V2: NavbarItem_V2[] = [
|
||||||
|
{
|
||||||
|
label: "Main Dashboard",
|
||||||
|
icon: "home",
|
||||||
|
link: "/admin/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Investasi",
|
||||||
|
icon: "wallet",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/investment",
|
||||||
|
// Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/investment/publish/status",
|
||||||
|
detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/investment/review/status",
|
||||||
|
detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/investment/reject/status",
|
||||||
|
detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Donasi",
|
||||||
|
icon: "hand-right",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/donation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/donation/publish/status",
|
||||||
|
detailPattern: "/admin/donation/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/donation/review/status",
|
||||||
|
detailPattern: "/admin/donation/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/donation/reject/status",
|
||||||
|
detailPattern: "/admin/donation/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori",
|
||||||
|
link: "/admin/donation/category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Event",
|
||||||
|
icon: "calendar-clear",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/event/publish/status",
|
||||||
|
detailPattern: "/admin/event/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/event/review/status",
|
||||||
|
detailPattern: "/admin/event/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/event/reject/status",
|
||||||
|
detailPattern: "/admin/event/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tipe Acara",
|
||||||
|
link: "/admin/event/type-of-event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/event/history/status",
|
||||||
|
detailPattern: "/admin/event/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Voting",
|
||||||
|
icon: "accessibility-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/voting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/voting/publish/status",
|
||||||
|
detailPattern: "/admin/voting/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/voting/review/status",
|
||||||
|
detailPattern: "/admin/voting/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/voting/reject/status",
|
||||||
|
detailPattern: "/admin/voting/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/voting/history",
|
||||||
|
detailPattern: "/admin/voting/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Job",
|
||||||
|
icon: "desktop-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/job/publish/status",
|
||||||
|
detailPattern: "/admin/job/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/job/review/status",
|
||||||
|
detailPattern: "/admin/job/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/job/reject/status",
|
||||||
|
detailPattern: "/admin/job/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Forum",
|
||||||
|
icon: "chatbubble-ellipses-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/forum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Posting",
|
||||||
|
link: "/admin/forum/posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Posting",
|
||||||
|
link: "/admin/forum/report-posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Komentar",
|
||||||
|
link: "/admin/forum/report-comment",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Collaboration",
|
||||||
|
icon: "people",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/collaboration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/collaboration/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Group",
|
||||||
|
link: "/admin/collaboration/group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/collaboration/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Maps",
|
||||||
|
icon: "map",
|
||||||
|
link: "/admin/maps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "App Information",
|
||||||
|
icon: "information-circle",
|
||||||
|
link: "/admin/app-information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User Access",
|
||||||
|
icon: "people",
|
||||||
|
link: "/admin/user-access",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const superAdminListMenu_V2: NavbarItem_V2[] = [
|
||||||
|
{
|
||||||
|
label: "Main Dashboard",
|
||||||
|
icon: "home",
|
||||||
|
link: "/admin/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Investasi",
|
||||||
|
icon: "wallet",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/investment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/investment/publish/status",
|
||||||
|
detailPattern: "/admin/investment/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/investment/review/status",
|
||||||
|
detailPattern: "/admin/investment/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/investment/reject/status",
|
||||||
|
detailPattern: "/admin/investment/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Donasi",
|
||||||
|
icon: "hand-right",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/donation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/donation/publish/status",
|
||||||
|
detailPattern: "/admin/donation/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/donation/review/status",
|
||||||
|
detailPattern: "/admin/donation/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/donation/reject/status",
|
||||||
|
detailPattern: "/admin/donation/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori",
|
||||||
|
link: "/admin/donation/category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Event",
|
||||||
|
icon: "calendar-clear",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/event/publish/status",
|
||||||
|
detailPattern: "/admin/event/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/event/review/status",
|
||||||
|
detailPattern: "/admin/event/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/event/reject/status",
|
||||||
|
detailPattern: "/admin/event/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tipe Acara",
|
||||||
|
link: "/admin/event/type-of-event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/event/history/status",
|
||||||
|
detailPattern: "/admin/event/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Voting",
|
||||||
|
icon: "accessibility-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/voting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/voting/publish/status",
|
||||||
|
detailPattern: "/admin/voting/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/voting/review/status",
|
||||||
|
detailPattern: "/admin/voting/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/voting/reject/status",
|
||||||
|
detailPattern: "/admin/voting/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/voting/history",
|
||||||
|
detailPattern: "/admin/voting/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Job",
|
||||||
|
icon: "desktop-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/job/publish/status",
|
||||||
|
detailPattern: "/admin/job/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/job/review/status",
|
||||||
|
detailPattern: "/admin/job/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/job/reject/status",
|
||||||
|
detailPattern: "/admin/job/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Forum",
|
||||||
|
icon: "chatbubble-ellipses-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/forum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Posting",
|
||||||
|
link: "/admin/forum/posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Posting",
|
||||||
|
link: "/admin/forum/report-posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Komentar",
|
||||||
|
link: "/admin/forum/report-comment",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Collaboration",
|
||||||
|
icon: "people",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/collaboration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/collaboration/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Group",
|
||||||
|
link: "/admin/collaboration/group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/collaboration/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Maps",
|
||||||
|
icon: "map",
|
||||||
|
link: "/admin/maps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "App Information",
|
||||||
|
icon: "information-circle",
|
||||||
|
link: "/admin/app-information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User Access",
|
||||||
|
icon: "people",
|
||||||
|
link: "/admin/user-access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Super Admin",
|
||||||
|
icon: "globe",
|
||||||
|
link: "/admin/super-admin",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
=================================================================================
|
||||||
|
PENJELASAN detailPattern:
|
||||||
|
=================================================================================
|
||||||
|
|
||||||
|
detailPattern digunakan untuk match dengan URL detail page yang strukturnya:
|
||||||
|
/admin/{module}/[id]/[status]
|
||||||
|
|
||||||
|
Contoh untuk Job Review:
|
||||||
|
- Link: /admin/job/review/status (halaman list review)
|
||||||
|
- detailPattern: /admin/job/* /review (detail dari review)
|
||||||
|
- Match dengan: /admin/job/123/review, /admin/job/456/review, dll
|
||||||
|
|
||||||
|
Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric).
|
||||||
|
|
||||||
|
Modul yang PERLU detailPattern:
|
||||||
|
✅ Investasi - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
✅ Donasi - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status])
|
||||||
|
✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status])
|
||||||
|
✅ Job - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
|
||||||
|
Modul yang TIDAK PERLU detailPattern:
|
||||||
|
❌ Forum - posting, report-posting, report-comment (struktur berbeda)
|
||||||
|
❌ Collaboration - struktur berbeda
|
||||||
|
❌ Maps, App Information, User Access - single page
|
||||||
|
❌ Dashboard submenu - auto-match dengan parent path
|
||||||
|
|
||||||
|
=================================================================================
|
||||||
|
*/
|
||||||
21
screens/Donation/BoxNews.tsx
Normal file
21
screens/Donation/BoxNews.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
|
||||||
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
|
|
||||||
|
export default function Donation_BoxNews({item}: {item: any}){
|
||||||
|
return <>
|
||||||
|
<BaseBox href={`/donation/[id]/(news)/${item?.id}`}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom truncate bold>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<TextCustom size="small">
|
||||||
|
{formatChatTime(item?.createdAt)}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ export default function Donation_ButtonStatusSection({
|
|||||||
}) {
|
}) {
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [isLoadingDelete, setLoadingDelete] = useState(false);
|
const [isLoadingDelete, setLoadingDelete] = useState(false);
|
||||||
|
const path: any = (status: string) => {
|
||||||
|
return `/donation/(tabs)/status?status=${status}`;
|
||||||
|
};
|
||||||
const handleBatalkanReview = async () => {
|
const handleBatalkanReview = async () => {
|
||||||
AlertDefaultSystem({
|
AlertDefaultSystem({
|
||||||
title: "Batalkan Review",
|
title: "Batalkan Review",
|
||||||
@@ -43,7 +46,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.push(path("draft"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -80,7 +83,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.replace(path("review"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -117,7 +120,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.replace(path("draft"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import { View } from "react-native";
|
|||||||
export default function Donation_ComponentBoxDetailData({
|
export default function Donation_ComponentBoxDetailData({
|
||||||
bottomSection,
|
bottomSection,
|
||||||
data,
|
data,
|
||||||
|
showSisaHari = true,
|
||||||
sisaHari,
|
sisaHari,
|
||||||
reminder,
|
reminder,
|
||||||
}: {
|
}: {
|
||||||
bottomSection?: React.ReactNode;
|
bottomSection?: React.ReactNode;
|
||||||
data: any;
|
data: any;
|
||||||
|
showSisaHari?: boolean;
|
||||||
sisaHari: number;
|
sisaHari: number;
|
||||||
reminder: boolean;
|
reminder: boolean;
|
||||||
}) {
|
}) {
|
||||||
@@ -34,9 +36,9 @@ export default function Donation_ComponentBoxDetailData({
|
|||||||
<TextCustom bold color="red">
|
<TextCustom bold color="red">
|
||||||
Waktu berakhir
|
Waktu berakhir
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
) : (
|
) : showSisaHari ? (
|
||||||
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
|
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
|
||||||
)}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
62
screens/Donation/ScreenBeranda.tsx
Normal file
62
screens/Donation/ScreenBeranda.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import FloatingButton from "@/components/Button/FloatingButton";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
|
||||||
|
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
export default function Donation_ScreenBeranda() {
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, search) => {
|
||||||
|
return await apiDonationGetAll({
|
||||||
|
category: "beranda",
|
||||||
|
page: String(page),
|
||||||
|
}).then((res) => {
|
||||||
|
console.log("RES", JSON.stringify(res, null, 2));
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
|
||||||
|
dependencies: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada donasi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Donation_BoxPublish data={item} id={item.id} />
|
||||||
|
)}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton onPress={() => router.push("/donation/create")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
174
screens/Donation/ScreenFundDisbursement.tsx
Normal file
174
screens/Donation/ScreenFundDisbursement.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
BaseBox,
|
||||||
|
Grid,
|
||||||
|
InformationBox,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import {
|
||||||
|
apiDonationDisbursementOfFundsListById,
|
||||||
|
apiDonationGetOne,
|
||||||
|
} from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { Divider } from "react-native-paper";
|
||||||
|
|
||||||
|
interface Donation_ScreenFundDisbursementProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenFundDisbursement({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenFundDisbursementProps) {
|
||||||
|
const [data, setData] = useState({
|
||||||
|
totalPencairan: 0,
|
||||||
|
akumulasiPencairan: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ambil data utama (total pencairan, dll) terpisah dari pagination
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onLoadData = async () => {
|
||||||
|
try {
|
||||||
|
const responseData = await apiDonationGetOne({
|
||||||
|
id: donationId,
|
||||||
|
category: "permanent",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
setData({
|
||||||
|
totalPencairan: responseData.data.totalPencairan,
|
||||||
|
akumulasiPencairan: responseData.data.akumulasiPencairan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadData();
|
||||||
|
}, [donationId]);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationDisbursementOfFundsListById({
|
||||||
|
id: donationId,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BaseBox key={index}>
|
||||||
|
<StackCustom>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom bold>{item?.title}</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<TextCustom size="small">
|
||||||
|
{dayjs(item?.createdAt).format("DD MMM YYYY")}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<TextCustom>{item?.deskripsi}</TextCustom>
|
||||||
|
{/* <Spacing /> */}
|
||||||
|
<Divider />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8} style={{ alignSelf: "center" }}>
|
||||||
|
<TextCustom bold size={"large"}>
|
||||||
|
Rp. {formatCurrencyDisplay(item?.nominalCair)}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<ActionIcon
|
||||||
|
icon={<Feather name="file-text" color={MainColor.black} />}
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate(
|
||||||
|
`/(application)/(image)/preview-image/${item?.imageId}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
{/*
|
||||||
|
<ButtonCenteredOnly
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate(
|
||||||
|
`/(application)/(image)/preview-image/${item?.imageId}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
icon="file-text"
|
||||||
|
>
|
||||||
|
Bukti Transaksi
|
||||||
|
</ButtonCenteredOnly> */}
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen header yang akan ditampilkan di atas daftar
|
||||||
|
const ListHeaderComponent = (
|
||||||
|
<View>
|
||||||
|
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
|
||||||
|
<BaseBox>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size="small">Total Pencairan Dana</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
{data?.akumulasiPencairan} kali
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
screens/Donation/ScreenListOfDonatur.tsx
Normal file
98
screens/Donation/ScreenListOfDonatur.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BaseBox,
|
||||||
|
Grid,
|
||||||
|
LoaderCustom,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationListOfDonaturById } from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { FontAwesome6 } from "@expo/vector-icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
|
||||||
|
interface Donation_ScreenListOfDonaturProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenListOfDonatur({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenListOfDonaturProps) {
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationListOfDonaturById({
|
||||||
|
id: donationId,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BaseBox key={index}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col
|
||||||
|
span={3}
|
||||||
|
style={{ alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<FontAwesome6
|
||||||
|
name="face-smile-wink"
|
||||||
|
size={50}
|
||||||
|
style={{ color: MainColor.yellow }}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<TextCustom bold size="large">
|
||||||
|
{item?.Author?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<Spacing />
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
|
||||||
|
<TextCustom bold size="large" color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom>
|
||||||
|
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada donatur",
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
screens/Donation/ScreenListOfNews.tsx
Normal file
97
screens/Donation/ScreenListOfNews.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { BackButton, DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
||||||
|
import { router, Stack } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Donation_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface Donation_ScreenListOfNewsProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenListOfNews({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenListOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetNewsById({
|
||||||
|
id: donationId,
|
||||||
|
category: "get-all",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donation_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada kabar",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Daftar Kabar",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
icon: <IconPlus />,
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/donation/${donationId}/(news)/add-news`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
console.log("PATH ", item.path);
|
||||||
|
router.navigate(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
screens/Donation/ScreenMyDonation.tsx
Normal file
152
screens/Donation/ScreenMyDonation.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
BaseBox,
|
||||||
|
DummyLandscapeImage,
|
||||||
|
Grid,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import FloatingButton from "@/components/Button/FloatingButton";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
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 { apiDonationGetAll } from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { Href, router } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export default function Donation_ScreenMyDonation() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, search) => {
|
||||||
|
if (!user?.id) {
|
||||||
|
throw new Error("User tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await apiDonationGetAll({
|
||||||
|
category: "my-donation",
|
||||||
|
authorId: user?.id,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
|
||||||
|
dependencies: [user?.id], // Reload ketika user.id berubah
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada transaksi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlerColor = (status: string) => {
|
||||||
|
if (status === "menunggu") {
|
||||||
|
return "orange";
|
||||||
|
} else if (status === "proses") {
|
||||||
|
return "white";
|
||||||
|
} else if (status === "berhasil") {
|
||||||
|
return "green";
|
||||||
|
} else if (status === "gagal") {
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePress = ({
|
||||||
|
invoiceId,
|
||||||
|
donationId,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
invoiceId: string;
|
||||||
|
donationId: string;
|
||||||
|
status: string;
|
||||||
|
}) => {
|
||||||
|
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
|
||||||
|
if (status === "menunggu") {
|
||||||
|
router.push(`${url}/invoice`);
|
||||||
|
} else if (status === "proses") {
|
||||||
|
router.push(`${url}/process`);
|
||||||
|
} else if (status === "berhasil") {
|
||||||
|
router.push(`${url}/success`);
|
||||||
|
} else if (status === "gagal") {
|
||||||
|
router.push(`${url}/failed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: any }) => (
|
||||||
|
<BaseBox
|
||||||
|
paddingTop={7}
|
||||||
|
paddingBottom={7}
|
||||||
|
onPress={() => {
|
||||||
|
handlePress({
|
||||||
|
status: _.lowerCase(item.statusInvoice),
|
||||||
|
invoiceId: item.id,
|
||||||
|
donationId: item.donasiId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={5}>
|
||||||
|
<DummyLandscapeImage
|
||||||
|
height={100}
|
||||||
|
unClickPath
|
||||||
|
imageId={item.imageId}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={1}>
|
||||||
|
<View />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2} bold>
|
||||||
|
{item.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(item.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<BadgeCustom
|
||||||
|
variant="light"
|
||||||
|
color={handlerColor(_.lowerCase(item.statusInvoice))}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{item.statusInvoice}
|
||||||
|
</BadgeCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton onPress={() => router.push("/donation/create")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
screens/Donation/ScreenRecapOfNews.tsx
Normal file
105
screens/Donation/ScreenRecapOfNews.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BackButton,
|
||||||
|
DotButton,
|
||||||
|
DrawerCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
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 { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Donation_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface Donation_ScreenRecapOfNewsProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenRecapOfNews({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenRecapOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetNewsById({
|
||||||
|
id: donationId,
|
||||||
|
category: "get-all",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [donationId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donation_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada kabar",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Rekap Kabar",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NewWrapper
|
||||||
|
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={[
|
||||||
|
{
|
||||||
|
icon: <IconPlus />,
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/donation/${donationId}/(news)/add-news`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
console.log("PATH ", item.path);
|
||||||
|
router.navigate(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
screens/Donation/ScreenStatus.tsx
Normal file
97
screens/Donation/ScreenStatus.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { ScrollableCustom } from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
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 { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||||
|
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
||||||
|
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
||||||
|
import { useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
interface DonationStatusProps {
|
||||||
|
initialStatus?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenStatus({
|
||||||
|
initialStatus = "publish",
|
||||||
|
}: DonationStatusProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||||
|
initialStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetByStatus({
|
||||||
|
authorId: user?.id as string,
|
||||||
|
status: activeCategory as string,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [user?.id, activeCategory],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [user?.id, activeCategory]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
pagination.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollComponent = (
|
||||||
|
<ScrollableCustom
|
||||||
|
data={dummyMasterStatus.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as any}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donasi_BoxStatus data={item} status={activeCategory as string} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||||
|
skeletonCount: 5,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
headerComponent={scrollComponent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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") {
|
||||||
|
|||||||
@@ -52,15 +52,18 @@ export async function apiAdminDonationUpdateStatus({
|
|||||||
export async function apiAdminDonationListOfDonatur({
|
export async function apiAdminDonationListOfDonatur({
|
||||||
id,
|
id,
|
||||||
status,
|
status,
|
||||||
|
page = "1"
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
status: "berhasil" | "gagal" | "proses" | "menunggu" | null;
|
status: "berhasil" | "gagal" | "proses" | "menunggu" | null;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
const query = status && status !== null ? `?status=${status}` : "";
|
const statusQuery = status && status !== null ? `&status=${status}` : "";
|
||||||
|
const pageQuery = `&page=${page}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/donation/${id}/donatur${query}`
|
`/mobile/admin/donation/${id}/donatur?${statusQuery}${pageQuery}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -105,19 +108,6 @@ export async function apiAdminDonationInvoiceUpdateById({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiAdminDonationListOfDonaturById({
|
|
||||||
id,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const response = await apiConfig.get(`/mobile/donation/${id}/donatur`);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiAdminDonationDisbursementOfFundsCreated({
|
export async function apiAdminDonationDisbursementOfFundsCreated({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -3,15 +3,31 @@ import { apiConfig } from "../api-config";
|
|||||||
export const apiAdminUserAccessGetAll = async ({
|
export const apiAdminUserAccessGetAll = async ({
|
||||||
search,
|
search,
|
||||||
category,
|
category,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
search?: string;
|
search?: string;
|
||||||
category: "only-user" | "only-admin" | "all-role";
|
category: "only-user" | "only-admin" | "all-role";
|
||||||
|
page?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/admin/user?category=${category}&search=${search}`);
|
const response = await apiConfig.get(
|
||||||
return response.data;
|
`/mobile/admin/user?category=${category}&search=${search}&page=${page}`,
|
||||||
|
);
|
||||||
|
// Pastikan mengembalikan struktur data yang konsisten
|
||||||
|
return {
|
||||||
|
success: response.data.success,
|
||||||
|
message: response.data.message,
|
||||||
|
data: response.data.data || [], // Gunakan data yang sebenarnya atau array kosong
|
||||||
|
pagination: response.data.pagination, // Jika ada info pagination
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error fetching data",
|
||||||
|
data: [],
|
||||||
|
pagination: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,12 +52,15 @@ export const apiAdminUserAccessUpdateStatus = async ({
|
|||||||
category: "access" | "role";
|
category: "access" | "role";
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.put(`/mobile/admin/user/${id}?category=${category}`, {
|
const response = await apiConfig.put(
|
||||||
data: {
|
`/mobile/admin/user/${id}?category=${category}`,
|
||||||
active,
|
{
|
||||||
role,
|
data: {
|
||||||
|
active,
|
||||||
|
role,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -40,16 +40,19 @@ export async function apiDonationGetOne({
|
|||||||
export async function apiDonationGetByStatus({
|
export async function apiDonationGetByStatus({
|
||||||
authorId,
|
authorId,
|
||||||
status,
|
status,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
authorId: string;
|
authorId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
const authorQuery = `/${authorId}`;
|
const authorQuery = `/${authorId}`;
|
||||||
const statusQuery = `/${status}`;
|
const statusQuery = `/${status}`;
|
||||||
|
const pageQuery = `?page=${page}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/donation${authorQuery}${statusQuery}`
|
`/mobile/donation${authorQuery}${statusQuery}${pageQuery}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -106,14 +109,17 @@ export async function apiDonationUpdateData({
|
|||||||
export async function apiDonationGetAll({
|
export async function apiDonationGetAll({
|
||||||
category,
|
category,
|
||||||
authorId,
|
authorId,
|
||||||
|
page = "1"
|
||||||
}: {
|
}: {
|
||||||
category: "beranda" | "my-donation";
|
category: "beranda" | "my-donation";
|
||||||
authorId?: string;
|
authorId?: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
const authorQuery = authorId ? `&authorId=${authorId}` : "";
|
const authorQuery = authorId ? `&authorId=${authorId}` : "";
|
||||||
|
const pageQuery = `&page=${page}`;
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/donation?category=${category}${authorQuery}`
|
`/mobile/donation?category=${category}${authorQuery}${pageQuery}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -216,13 +222,15 @@ export async function apiDonationCreateNews({
|
|||||||
export async function apiDonationGetNewsById({
|
export async function apiDonationGetNewsById({
|
||||||
id,
|
id,
|
||||||
category,
|
category,
|
||||||
|
page = "1"
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
category: "get-all" | "get-one";
|
category: "get-all" | "get-one";
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/donation/${id}/news?category=${category}`
|
`/mobile/donation/${id}/news?category=${category}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -258,11 +266,28 @@ export async function apiDonationDeleteNews({ id }: { id: string }) {
|
|||||||
|
|
||||||
export async function apiDonationDisbursementOfFundsListById({
|
export async function apiDonationDisbursementOfFundsListById({
|
||||||
id,
|
id,
|
||||||
|
page = "1"
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/donation/${id}/disbursement`);
|
const response = await apiConfig.get(`/mobile/donation/${id}/disbursement?page=${page}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiDonationListOfDonaturById({
|
||||||
|
id,
|
||||||
|
page = "1"
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
page?: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const response = await apiConfig.get(`/mobile/donation/${id}/donatur?page=${page}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -31,5 +31,5 @@ export const formatChatTime = (date: string | Date): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lebih dari 7 hari lalu
|
// Lebih dari 7 hari lalu
|
||||||
return messageDate.format('DD - MM - YYYY, HH.mm'); // "05 - 11 - 2025, 14.00"
|
return messageDate.format('DD/MM/YYYY, HH.mm'); // "05/11/2025, 14.00"
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user