Compare commits
11 Commits
loaddata/9
...
fixed-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| f284e2ec02 | |||
| 1d61ad51e5 | |||
| 76845b71b4 | |||
| 97e1f50660 | |||
| 1cbe4ab330 | |||
| 42fa80c228 | |||
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 | |||
| 5c931b069c | |||
| b2be7be533 |
167
QWEN.md
167
QWEN.md
@@ -35,6 +35,7 @@ hipmi-mobile/
|
|||||||
│ └── ...
|
│ └── ...
|
||||||
├── assets/ # Images, icons, and static assets
|
├── assets/ # Images, icons, and static assets
|
||||||
├── constants/ # Constants and configuration values
|
├── constants/ # Constants and configuration values
|
||||||
|
├── helpers/ # Helper functions (pagination, etc.)
|
||||||
├── hooks/ # Custom React hooks
|
├── hooks/ # Custom React hooks
|
||||||
├── lib/ # Utility libraries
|
├── lib/ # Utility libraries
|
||||||
├── navigation/ # Navigation configuration
|
├── navigation/ # Navigation configuration
|
||||||
@@ -49,6 +50,8 @@ hipmi-mobile/
|
|||||||
- Node.js (with bun as the package manager)
|
- Node.js (with bun as the package manager)
|
||||||
- Expo CLI
|
- Expo CLI
|
||||||
- iOS Simulator or Android Emulator (for native builds)
|
- iOS Simulator or Android Emulator (for native builds)
|
||||||
|
- Android Studio (for Android builds)
|
||||||
|
- Xcode (for iOS builds, macOS only)
|
||||||
|
|
||||||
### Setup and Development
|
### Setup and Development
|
||||||
|
|
||||||
@@ -76,28 +79,79 @@ hipmi-mobile/
|
|||||||
bun run lint
|
bun run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Build Commands
|
||||||
The application uses environment variables defined in the app.config.js file:
|
|
||||||
|
#### EAS Build (Production)
|
||||||
|
```bash
|
||||||
|
# Production build
|
||||||
|
eas build --profile production
|
||||||
|
|
||||||
|
# Preview build
|
||||||
|
eas build --profile preview
|
||||||
|
|
||||||
|
# Development build
|
||||||
|
eas build --profile development
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Local Native Builds
|
||||||
|
```bash
|
||||||
|
# Generate native folders (iOS & Android)
|
||||||
|
npx expo prebuild
|
||||||
|
|
||||||
|
# iOS specific
|
||||||
|
bunx expo prebuild --platform ios
|
||||||
|
open ios/HIPMIBADUNG.xcworkspace
|
||||||
|
|
||||||
|
# Android specific
|
||||||
|
bunx expo prebuild --platform android
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version Management
|
||||||
|
```bash
|
||||||
|
# Patch version update
|
||||||
|
npm version patch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android Debugging
|
||||||
|
```bash
|
||||||
|
# List connected devices
|
||||||
|
adb devices
|
||||||
|
|
||||||
|
# Install APK to device/emulator
|
||||||
|
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
# Install to specific device
|
||||||
|
adb -s <device_id> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The application uses environment variables defined in the `app.config.js` file:
|
||||||
- `API_BASE_URL`: Base URL for API endpoints
|
- `API_BASE_URL`: Base URL for API endpoints
|
||||||
- `BASE_URL`: Base application URL
|
- `BASE_URL`: Base application URL
|
||||||
- `DEEP_LINK_URL`: URL for deep linking functionality
|
- `DEEP_LINK_URL`: URL for deep linking functionality
|
||||||
|
|
||||||
### EAS Build Configuration
|
Create a `.env` file in the project root with these variables.
|
||||||
|
|
||||||
|
## EAS Build Configuration
|
||||||
|
|
||||||
The project uses Expo Application Services (EAS) for building and deploying:
|
The project uses Expo Application Services (EAS) for building and deploying:
|
||||||
- Development builds with development client
|
- **Development**: Development builds with development client
|
||||||
- Preview builds for internal distribution
|
- **Preview**: Internal distribution builds (APK for Android)
|
||||||
- Production builds for app stores
|
- **Production**: App store builds (App Bundle for Android, IPA for iOS)
|
||||||
|
|
||||||
|
Configuration is in `eas.json`.
|
||||||
|
|
||||||
## Features and Functionality
|
## Features and Functionality
|
||||||
|
|
||||||
The application appears to include several key modules:
|
The application includes several key modules:
|
||||||
- **Authentication**: Login, registration, and verification flows
|
- **Authentication**: Login with phone number, OTP verification, registration, terms acceptance
|
||||||
- **Admin Panel**: Administrative functions
|
- **Admin Panel**: Administrative functions for managing content and users
|
||||||
- **Collaboration**: Tools for member collaboration
|
- **Collaboration**: Tools for member collaboration
|
||||||
- **Events**: Event management and calendar
|
- **Events**: Event management and calendar
|
||||||
- **Forum**: Discussion forums
|
- **Forum**: Discussion forums
|
||||||
- **Maps**: Location-based services with Mapbox integration
|
- **Maps**: Location-based services with Mapbox integration
|
||||||
- **Donations**: Donation functionality
|
- **Donations**: Donation functionality with fund disbursement tracking
|
||||||
- **Job Board**: Employment opportunities
|
- **Job Board**: Employment opportunities
|
||||||
- **Investment**: Investment-related features
|
- **Investment**: Investment-related features
|
||||||
- **Voting**: Voting systems
|
- **Voting**: Voting systems
|
||||||
@@ -109,18 +163,50 @@ The application appears to include several key modules:
|
|||||||
### Coding Standards
|
### Coding Standards
|
||||||
- TypeScript is used throughout the project for type safety
|
- TypeScript is used throughout the project for type safety
|
||||||
- Component-based architecture with reusable components
|
- Component-based architecture with reusable components
|
||||||
- Context API for state management
|
- Context API for state management (AuthContext)
|
||||||
- File-based routing with Expo Router
|
- File-based routing with Expo Router
|
||||||
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||||
|
- Path aliases: `@/*` maps to project root
|
||||||
|
|
||||||
|
### Architecture Patterns
|
||||||
|
|
||||||
|
#### Screen Components
|
||||||
|
- Screen components are stored in `/screens` directory organized by feature
|
||||||
|
- Route files in `/app` import and use screen components
|
||||||
|
- Example pattern:
|
||||||
|
```tsx
|
||||||
|
// app/some-route.tsx
|
||||||
|
import SomeScreen from "@/screens/Feature/ScreenSome";
|
||||||
|
|
||||||
|
export default function SomeRoute() {
|
||||||
|
return <SomeScreen />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wrapper Components
|
||||||
|
- `NewWrapper` component is used for consistent screen layouts
|
||||||
|
- Located at `components/_ShareComponent/NewWrapper.tsx`
|
||||||
|
|
||||||
|
#### Pagination Pattern
|
||||||
|
- Use `hooks/use-pagination.tsx` and `helpers/paginationHelpers.tsx`
|
||||||
|
- Helper functions: `createSkeletonList`, `createEmptyState`, `createLoadingFooter`, `createPaginationComponents`
|
||||||
|
- API functions should accept `page` parameter (default: "1")
|
||||||
|
|
||||||
|
### API Service Structure
|
||||||
|
- Base API configuration: `service/api-config.ts`
|
||||||
|
- Client APIs: `service/api-client/`
|
||||||
|
- Admin APIs: `service/api-admin/`
|
||||||
|
- All API calls use axios with interceptors for auth token injection
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- Linting is configured with ESLint
|
- Linting is configured with ESLint
|
||||||
- Standard Expo linting configuration is used
|
- Standard Expo linting configuration
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- Firebase is integrated for authentication and messaging
|
- Firebase is integrated for authentication and messaging
|
||||||
- Camera and location permissions are properly configured
|
- Camera and location permissions are properly configured
|
||||||
- Deep linking is secured with app domain associations
|
- Deep linking is secured with app domain associations
|
||||||
|
- Auth tokens stored in AsyncStorage
|
||||||
|
|
||||||
## Key Dependencies
|
## Key Dependencies
|
||||||
|
|
||||||
@@ -133,6 +219,9 @@ The application appears to include several key modules:
|
|||||||
- `react-native-toast-message`: Toast notifications
|
- `react-native-toast-message`: Toast notifications
|
||||||
- `react-native-otp-entry`: OTP input components
|
- `react-native-otp-entry`: OTP input components
|
||||||
- `react-native-qrcode-svg`: QR code generation
|
- `react-native-qrcode-svg`: QR code generation
|
||||||
|
- `axios`: HTTP client for API calls
|
||||||
|
- `lodash`: Utility library
|
||||||
|
- `moti`: Animation library
|
||||||
|
|
||||||
### Development Dependencies
|
### Development Dependencies
|
||||||
- `@types/*`: TypeScript type definitions
|
- `@types/*`: TypeScript type definitions
|
||||||
@@ -142,28 +231,52 @@ The application appears to include several key modules:
|
|||||||
## Platform Support
|
## Platform Support
|
||||||
|
|
||||||
The application is configured to support:
|
The application is configured to support:
|
||||||
- **iOS**: With tablet support and proper permissions
|
- **iOS**:
|
||||||
- **Android**: With adaptive icons and intent filters for deep linking
|
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||||
|
- Supports tablets
|
||||||
|
- Build number: 21
|
||||||
|
- Google Services integration
|
||||||
|
- Associated domains for deep linking
|
||||||
|
- **Android**:
|
||||||
|
- Package name: `com.bip.hipmimobileapp`
|
||||||
|
- Version code: 4
|
||||||
|
- Adaptive icons
|
||||||
|
- Edge-to-edge display enabled
|
||||||
|
- Intent filters for HTTPS deep linking
|
||||||
- **Web**: Static output configuration for web deployment
|
- **Web**: Static output configuration for web deployment
|
||||||
|
|
||||||
## Special Configurations
|
## Special Configurations
|
||||||
|
|
||||||
### iOS Configuration
|
### Deep Linking
|
||||||
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
- Scheme: `hipmimobile://`
|
||||||
- Supports tablets
|
- Associated domains: `applinks:cld-dkr-staging-hipmi.wibudev.com`
|
||||||
- Google Services integration
|
- Configured for both iOS and Android
|
||||||
- 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
|
### Maps Integration
|
||||||
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||||
|
|
||||||
### Push Notifications
|
### Push Notifications
|
||||||
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||||
|
|
||||||
|
### Camera
|
||||||
|
Camera permissions configured for both iOS and Android with microphone access for recording.
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding a New Screen
|
||||||
|
1. Create screen component in appropriate `/screens` subdirectory
|
||||||
|
2. Add route in `/app` directory if needed
|
||||||
|
3. Configure navigation in `AppRoot.tsx` if custom header is needed
|
||||||
|
|
||||||
|
### Adding API Endpoint
|
||||||
|
1. Add function in appropriate service file (`service/api-client/` or `service/api-admin/`)
|
||||||
|
2. Use `apiConfig` axios instance for requests
|
||||||
|
3. Include proper error handling
|
||||||
|
|
||||||
|
### Refactoring Pattern (from docs/prompt-for-qwen-code.md)
|
||||||
|
When moving code from route files to screen components:
|
||||||
|
1. Create new file in `screens/<Feature>/` directory
|
||||||
|
2. Rename function with prefix (e.g., `Admin_`, `Donation_`)
|
||||||
|
3. Use `NewWrapper` component for consistent layout
|
||||||
|
4. Apply pagination helpers if displaying lists
|
||||||
|
5. Import and call from original route file
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default {
|
|||||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||||
},
|
},
|
||||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||||
buildNumber: "20",
|
buildNumber: "21",
|
||||||
},
|
},
|
||||||
|
|
||||||
android: {
|
android: {
|
||||||
|
|||||||
@@ -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,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,13 +27,14 @@ 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(() => {
|
||||||
@@ -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,7 +107,20 @@ export default function DonasiDetailStatus() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!data ? (
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Donation_ComponentBoxDetailData
|
<Donation_ComponentBoxDetailData
|
||||||
sisaHari={value.sisa}
|
sisaHari={value.sisa}
|
||||||
reminder={value.reminder}
|
reminder={value.reminder}
|
||||||
@@ -114,12 +140,17 @@ export default function DonasiDetailStatus() {
|
|||||||
dataStory={data?.CeritaDonasi}
|
dataStory={data?.CeritaDonasi}
|
||||||
/>
|
/>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
|
{data && (
|
||||||
<Donation_ButtonStatusSection
|
<Donation_ButtonStatusSection
|
||||||
id={id as string}
|
id={id as string}
|
||||||
status={status as string}
|
status={status as string}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</>
|
||||||
|
)}
|
||||||
|
</NewWrapper>
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
isVisible={openDrawer}
|
isVisible={openDrawer}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
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 {
|
||||||
@@ -200,7 +201,7 @@ export default function DonationEdit() {
|
|||||||
>
|
>
|
||||||
<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
|
||||||
|
|||||||
@@ -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,13 +98,21 @@ export default function DonasiDetailBeranda() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper footerComponent={buttonSection}>
|
<NewWrapper footerComponent={buttonSection}>
|
||||||
|
{!data ? (
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
) : (
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<Donation_ComponentBoxDetailData
|
<Donation_ComponentBoxDetailData
|
||||||
sisaHari={value.sisa}
|
sisaHari={value.sisa}
|
||||||
reminder={value.reminder}
|
reminder={value.reminder}
|
||||||
data={data}
|
data={data}
|
||||||
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
|
bottomSection={
|
||||||
|
<Donation_ProgressSection
|
||||||
|
id={id as string}
|
||||||
|
progres={Number(data?.progres) || 0}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
||||||
<Donation_ComponentStoryFunrising
|
<Donation_ComponentStoryFunrising
|
||||||
@@ -110,7 +120,8 @@ export default function DonasiDetailBeranda() {
|
|||||||
dataStory={data?.CeritaDonasi}
|
dataStory={data?.CeritaDonasi}
|
||||||
/>
|
/>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</ViewWrapper>
|
)}
|
||||||
|
</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,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,14 +61,31 @@ 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 (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function Portofolio() {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData(id as string);
|
onLoadData(id as string);
|
||||||
onLoadUserByToken();
|
onLoadUserByToken();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData(id: string) {
|
async function onLoadData(id: string) {
|
||||||
@@ -144,27 +144,27 @@ export default function Portofolio() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="building-o"
|
name="building-o"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
rightItem={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="list-outline"
|
name="list-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={
|
rightItem={
|
||||||
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
|
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -172,26 +172,26 @@ export default function Portofolio() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="call-outline"
|
name="call-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
|
rightItem={<TextCustom>{data?.tlpn}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
rightItem={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenBusinessFieldDetail } from "@/screens/Admin/App-Information/ScreenBusinessFieldDetail";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
BaseBox,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenBusinessFieldDetail />;
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadDetail();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadDetail = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiAdminMasterBusinessFieldById({
|
|
||||||
id: id as string,
|
|
||||||
category: "all",
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
setData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setData(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
<StackCustom>
|
|
||||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
|
||||||
|
|
||||||
{!data ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : (
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom bold>Nama Bidang</TextCustom>
|
|
||||||
<Spacing height={5} />
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom bold>
|
|
||||||
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</TextCustom>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
span1={10}
|
|
||||||
span2={2}
|
|
||||||
text1={
|
|
||||||
<TextCustom bold size={"large"}>
|
|
||||||
{data?.bidang?.name}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={<IconEdit size={16} color={MainColor.black} />}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${id}/bidang-update`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
{/* <Divider /> */}
|
|
||||||
<Spacing height={5} />
|
|
||||||
|
|
||||||
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
|
|
||||||
<Spacing height={5} />
|
|
||||||
|
|
||||||
{data?.subBidang?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<StackCustom gap={0}>
|
|
||||||
<TextCustom bold>
|
|
||||||
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
span1={10}
|
|
||||||
span2={2}
|
|
||||||
text1={
|
|
||||||
<TextCustom bold size={"large"}>
|
|
||||||
{item.name}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconEdit size={16} color={MainColor.black} />
|
|
||||||
}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,5 @@
|
|||||||
import { ScrollableCustom, StackCustom, ViewWrapper } from "@/components";
|
import { Admin_ScreenAppInformation } from "@/screens/Admin/App-Information/ScreenAppInformation";
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
|
||||||
import AdminAppInformation_Bank from "@/screens/Admin/App-Information/InformationBankSection";
|
|
||||||
import AdminAppInformation_StickerSection from "@/screens/Admin/App-Information/StickerSection";
|
|
||||||
import { router } from "expo-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Alert } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminInformation() {
|
export default function AdminInformation() {
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
return <Admin_ScreenAppInformation />;
|
||||||
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
setActivePage(item.label);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<StackCustom>
|
|
||||||
<ScrollableCustom
|
|
||||||
data={listPage}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (activeCategory) {
|
|
||||||
case "bank":
|
|
||||||
return <AdminAppInformation_Bank />;
|
|
||||||
case "business":
|
|
||||||
return <AdminAppInformation_BusinessFieldSection />;
|
|
||||||
case "sticker":
|
|
||||||
return <AdminAppInformation_StickerSection />;
|
|
||||||
default:
|
|
||||||
return <AdminAppInformation_Bank />;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={scrollComponent}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={activePage}
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
if (activeCategory === "bank") {
|
|
||||||
router.push("/admin/app-information/information-bank/create");
|
|
||||||
} else if (activeCategory === "business") {
|
|
||||||
router.push("/admin/app-information/business-field/create");
|
|
||||||
} else if (activeCategory === "sticker") {
|
|
||||||
Alert.alert("Coming Soon", "Next Update");
|
|
||||||
// router.push("/admin/app-information/sticker/create");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{renderContent()}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPage = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
label: "Informasi Bank",
|
|
||||||
value: "bank",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
label: "Bidang & Sub Bidang",
|
|
||||||
value: "business",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
label: "Stiker",
|
|
||||||
value: "sticker",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function AdminCollaborationPublish() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
handlerLoadData();
|
handlerLoadData();
|
||||||
}, [status])
|
}, [status]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlerLoadData = async () => {
|
const handlerLoadData = async () => {
|
||||||
@@ -83,8 +83,8 @@ export default function AdminCollaborationPublish() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>Catatan report</TextCustom>}
|
leftItem={<TextCustom bold>Catatan report</TextCustom>}
|
||||||
rightIcon={<TextCustom>{data?.report}</TextCustom>}
|
rightItem={<TextCustom>{data?.report}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,19 +42,19 @@ export default function AdminDonationDetailDisbursementOfFunds() {
|
|||||||
const listData = [
|
const listData = [
|
||||||
{
|
{
|
||||||
label: "Nominal",
|
label: "Nominal",
|
||||||
value: `Rp ${(data && formatCurrencyDisplay(data?.nominalCair)) || 0}`,
|
value: `Rp ${data ? formatCurrencyDisplay(data?.nominalCair) : 0}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tanggal",
|
label: "Tanggal",
|
||||||
value: dateTimeView({ date: data?.createdAt }),
|
value: data ? dateTimeView({ date: data?.createdAt }) : "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Judul",
|
label: "Judul",
|
||||||
value: (data && data?.title) || "-",
|
value: data ? data?.title : "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Deskripsi",
|
label: "Deskripsi",
|
||||||
value: (data && data?.deskripsi) || "-",
|
value: data ? data?.deskripsi : "-",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,126 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationListDisbursementOfFunds } from "@/screens/Admin/Donation/ScreenDonationListDisbursementOfFunds";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
CenterCustom,
|
|
||||||
Divider,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminDonasiListOfDisbursementOfFunds() {
|
export default function AdminDonasiListOfDisbursementOfFunds() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenDonationListDisbursementOfFunds />;
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationDisbursementOfFundsListById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle title="Daftar Pencairan Dana" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Tanggal
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Nominal
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/donation/${item?.id}/detail-disbursement-of-funds`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom align="center" truncate>
|
|
||||||
{dayjs(item?.createdAt).format("DD-MM-YYYY")}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom align="center" truncate>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominalCair)}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,186 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationListOfDonatur } from "@/screens/Admin/Donation/ScreenDonationListOfDonatur";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SelectCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { apiMasterTransaction } from "@/service/api-client/api-master";
|
|
||||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonasiListOfDonatur() {
|
export default function AdminDonasiListOfDonatur() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenDonationListOfDonatur />;
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
const [master, setMaster] = React.useState<any[]>([]);
|
|
||||||
|
|
||||||
const [selectValue, setSelectValue] = React.useState<string | null>(null);
|
|
||||||
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id, selectValue])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationListOfDonatur({
|
|
||||||
id: id as string,
|
|
||||||
status: selectedStatus as any,
|
|
||||||
});
|
|
||||||
// console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoadMaster();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onLoadMaster = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMasterTransaction();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setMaster(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setMaster([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchComponent = (
|
|
||||||
<View style={{ flexDirection: "row", gap: 5 }}>
|
|
||||||
<SelectCustom
|
|
||||||
placeholder="Pilih status transaksi"
|
|
||||||
data={
|
|
||||||
_.isEmpty(master)
|
|
||||||
? []
|
|
||||||
: master?.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
value={selectValue}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
setSelectValue(value);
|
|
||||||
const nameSelected = master.find((item: any) => item.id === value);
|
|
||||||
const statusChooses = _.lowerCase(nameSelected?.name);
|
|
||||||
setSelectedStatus(statusChooses);
|
|
||||||
}}
|
|
||||||
styleContainer={{ width: "100%", marginBottom: 0 }}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle newComponent={searchComponent} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Donatur
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Status
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/donation/${item?.id}/${_.lowerCase(
|
|
||||||
item?.DonasiMaster_StatusInvoice?.name
|
|
||||||
)}/transaction-detail`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center" truncate>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "center" }}
|
|
||||||
color={colorBadgeTransaction({
|
|
||||||
status: item?.DonasiMaster_StatusInvoice?.name,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item?.DonasiMaster_StatusInvoice?.name}
|
|
||||||
</BadgeCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationStatus } from "@/screens/Admin/Donation/ScreenDonationStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonationStatus() {
|
export default function AdminDonationStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenDonationStatus />;
|
||||||
console.log("[STATUS]", status);
|
|
||||||
|
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
const [loadData, setLoadData] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonation({
|
|
||||||
category: status as "publish" | "review" | "reject",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[RES]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={(value) => setSearch(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Donasi" />}>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Donasi"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(data) ? (
|
|
||||||
<TextCustom align="center" size="small" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
data?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export default function AdminDonationCategoryUpdate() {
|
|||||||
const response = await apiAdminMasterDonationCategoryById({
|
const response = await apiAdminMasterDonationCategoryById({
|
||||||
id: id as any,
|
id: id as any,
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
};
|
};
|
||||||
@@ -44,10 +43,9 @@ export default function AdminDonationCategoryUpdate() {
|
|||||||
id: id as any,
|
id: id as any,
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
router.back();
|
router.back();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log("Error update category:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
import {
|
import { Admin_ScreenDonationCategory } from "@/screens/Admin/Donation/ScreenDonationCategory";
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
ClickableCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { RefreshControl, View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonationCategory() {
|
export default function AdminDonationCategory() {
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
return <Admin_ScreenDonationCategory />;
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
fetchMaster();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchMaster = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await apiAdminMasterDonationCategory();
|
|
||||||
if (response.success) {
|
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
setListData(response.data);
|
|
||||||
} else {
|
|
||||||
setListData([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[Error]", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRefresh = async () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
await fetchMaster();
|
|
||||||
setRefreshing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
||||||
}
|
|
||||||
headerComponent={<AdminTitlePage title="Donasi" />}
|
|
||||||
>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Kategori"
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/category-create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Status</TextCustom>}
|
|
||||||
value={<TextCustom bold>Kategori</TextCustom>}
|
|
||||||
/>
|
|
||||||
{/* <Grid>
|
|
||||||
<Grid.Col style={{ paddingLeft: 10 }} span={4}>
|
|
||||||
<TextCustom bold>Status</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>Kategori</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid> */}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{listData.map((item, index) => (
|
|
||||||
<ClickableCustom
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/category-update?id=${item.id}`);
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={colorActivationForBadge({
|
|
||||||
status: item.active,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
value={<TextCustom>{item.name}</TextCustom>}
|
|
||||||
/>
|
|
||||||
{/* <Grid containerStyle={{ paddingBottom: 10 }}>
|
|
||||||
<Grid.Col span={4} style={{ paddingLeft: 10 }}>
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={item.active ? MainColor.green : MainColor.red}
|
|
||||||
>
|
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>{item.name}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid> */}
|
|
||||||
<Divider />
|
|
||||||
</ClickableCustom>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
</View>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export default function AdminEventDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextCustom align="center">{isDevLink}</TextCustom>
|
{/* <TextCustom align="center">{isDevLink}</TextCustom> */}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,105 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { View } from "moti";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function AdminEventListOfParticipants() {
|
export default function AdminEventListOfParticipants() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenEventListOfParticipants />;
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
|
||||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminEventListOfParticipants({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
setStartDate(dayjs(response.data.Event.tanggal));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
|
||||||
>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada peserta
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
{item?.User?.username}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
{startDate &&
|
|
||||||
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "flex-end" }}
|
|
||||||
color={item?.isPresent ? "green" : "red"}
|
|
||||||
>
|
|
||||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
|
||||||
</BadgeCustom>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "flex-end" }}
|
|
||||||
color="gray"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</BadgeCustom>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
ClickableCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminEventStatus() {
|
export default function AdminEventStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenEventStatus />;
|
||||||
console.log("[STATUS EVENT]", status);
|
|
||||||
|
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminEvent({
|
|
||||||
category: status as "publish" | "review" | "reject" | "history" as any,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[RES LIST BY STATUS: ${status}]`,
|
|
||||||
JSON.stringify(response, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={(value) => setSearch(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Username"
|
|
||||||
title2="Tanggal"
|
|
||||||
title3="Judul Event"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" size="small" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<ClickableCustom
|
|
||||||
key={index}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
// <ActionIcon
|
|
||||||
// icon={
|
|
||||||
// <Octicons
|
|
||||||
// name="eye"
|
|
||||||
// size={ICON_SIZE_BUTTON}
|
|
||||||
// color="black"
|
|
||||||
// />
|
|
||||||
// }
|
|
||||||
// onPress={() => {
|
|
||||||
// router.push(`/admin/event/${item.id}/${status}`);
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{dateTimeView({ date: item?.tanggal })}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider/>
|
|
||||||
</ClickableCustom>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,5 @@
|
|||||||
import {
|
import { Admin_ScreenEventTypeOfEvent } from "@/screens/Admin/Voting/ScreenEventTypeOfEvent";
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminEventTypeOfEvent() {
|
export default function AdminEventTypeOfEvent() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenEventTypeOfEvent />;
|
||||||
const [loadData, setLoadData] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminMasterTypeOfEvent();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]",error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Tipe Acara"
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/type-create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={<TextCustom bold align="center">Status</TextCustom>}
|
|
||||||
component3={<TextCustom bold>Tipe Acara</TextCustom>}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconEdit size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/type-update?id=${item.id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
style2={{ alignItems: "center" }}
|
|
||||||
component2={
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={colorActivationForBadge({
|
|
||||||
status: item?.active,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item?.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component3={<TextCustom >{item.name}</TextCustom>}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default function AdminInvestmentTransactionDetail() {
|
|||||||
spanRight={6}
|
spanRight={6}
|
||||||
styleLeft={{ paddingRight: 10 }}
|
styleLeft={{ paddingRight: 10 }}
|
||||||
styleRight={{ paddingLeft: 10 }}
|
styleRight={{ paddingLeft: 10 }}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -181,7 +181,7 @@ export default function AdminInvestmentTransactionDetail() {
|
|||||||
Tolak
|
Tolak
|
||||||
</ButtonCustom>
|
</ButtonCustom>
|
||||||
}
|
}
|
||||||
rightIcon={
|
rightItem={
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
BaseBox,
|
BaseBox,
|
||||||
DummyLandscapeImage,
|
DummyLandscapeImage,
|
||||||
Grid,
|
Grid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
@@ -120,7 +121,7 @@ export default function AdminJobDetailStatus() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||||
>
|
>
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
@@ -184,7 +185,7 @@ export default function AdminJobDetailStatus() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
AlertDefaultSystem,
|
AlertDefaultSystem,
|
||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
|
NewWrapper,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
@@ -100,7 +101,7 @@ export default function AdminJobRejectInput() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
footerComponent={buttonSubmit}
|
footerComponent={buttonSubmit}
|
||||||
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
||||||
>
|
>
|
||||||
@@ -112,7 +113,7 @@ export default function AdminJobRejectInput() {
|
|||||||
showCount
|
showCount
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
/>
|
/>
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminJobStatus() {
|
export default function AdminJobStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenJobStatus />;
|
||||||
const [list, setList] = useState<any | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
handlerLoadList();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminJob({
|
|
||||||
category: status as "publish" | "review" | "reject",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
placeholder="Cari"
|
|
||||||
onChangeText={setSearch}
|
|
||||||
value={search}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Job Vacancy" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Pekerjaan"
|
|
||||||
/>
|
|
||||||
{/* <Spacing /> */}
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/job/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom align="center" truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2} align="center">
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { ButtonCustom, DrawerCustom, DummyLandscapeImage, Grid, Spacing, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
import {
|
||||||
|
ButtonCustom,
|
||||||
|
DrawerCustom,
|
||||||
|
DummyLandscapeImage,
|
||||||
|
Grid,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
ViewWrapper,
|
||||||
|
} from "@/components";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
@@ -45,7 +54,7 @@ export default function AdminMaps() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
handlerLoadList();
|
handlerLoadList();
|
||||||
}, [])
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
const handlerLoadList = async () => {
|
||||||
@@ -144,52 +153,52 @@ export default function AdminMaps() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="building-o"
|
name="building-o"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
|
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="list-outline"
|
name="list-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="call-outline"
|
name="call-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
rightItem={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function SuperAdminDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -48,7 +48,7 @@ export default function SuperAdminDetail() {
|
|||||||
const response = await apiAdminUserAccessUpdateStatus({
|
const response = await apiAdminUserAccessUpdateStatus({
|
||||||
id: id as string,
|
id: id as string,
|
||||||
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
||||||
category: "role"
|
category: "role",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
@@ -102,8 +102,8 @@ export default function SuperAdminDetail() {
|
|||||||
key={index}
|
key={index}
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function AdminUserAccessDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -102,8 +102,8 @@ export default function AdminUserAccessDetail() {
|
|||||||
key={index}
|
key={index}
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
BaseBox,
|
BaseBox,
|
||||||
CircleContainer,
|
CircleContainer,
|
||||||
Grid,
|
Grid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
import ReportBox from "@/components/Box/ReportBox";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -40,7 +41,7 @@ export default function AdminVotingDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -169,23 +170,25 @@ export default function AdminVotingDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||||
>
|
>
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
{listData.map((item, i) => (
|
{listData.map((item, i) => (
|
||||||
<GridSpan_4_8
|
<GridTwoView
|
||||||
key={i}
|
key={i}
|
||||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
spanLeft={5}
|
||||||
value={<TextCustom>{item.value}</TextCustom>}
|
spanRight={7}
|
||||||
|
leftItem={<TextCustom bold>{item.label}</TextCustom>}
|
||||||
|
rightItem={<TextCustom>{item.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
{status === "publish" ||
|
{(status === "publish" || status === "history") && (
|
||||||
(status === "history" && (
|
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
<TextCustom bold align="center">
|
<TextCustom bold align="center">
|
||||||
Hasil Voting
|
Hasil Voting
|
||||||
@@ -209,11 +212,11 @@ export default function AdminVotingDetail() {
|
|||||||
</TextCustom>
|
</TextCustom>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
{data &&
|
{data &&
|
||||||
data?.catatan &&
|
data?.catatan &&
|
||||||
@@ -250,7 +253,7 @@ export default function AdminVotingDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenVotingStatus } from "@/screens/Admin/Voting/ScreenVotingStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminVotingStatus() {
|
export default function AdminVotingStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenVotingStatus />;
|
||||||
const [list, setList] = useState<any | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminVoting({
|
|
||||||
category: status as "publish" | "review" | "reject" as any,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Voting"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" bold color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list.map((item: any, i: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={i}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/voting/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenVotingHistory } from "@/screens/Admin/Voting/ScreenVotingHistory";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminVotingHistory() {
|
export default function AdminVotingHistory() {
|
||||||
const [list, setList] = useState<any | null>(null);
|
return <Admin_ScreenVotingHistory />;
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [ search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminVoting({
|
|
||||||
category: "history",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Riwayat"
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Voting"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? <LoaderCustom/> : _.isEmpty(list) ? <TextCustom align="center" bold color="gray">Belum ada data</TextCustom> : list.map((item: any, i: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={i}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/voting/${item.id}/history`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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];
|
||||||
}
|
}
|
||||||
}, [normalizedPathname]);
|
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, 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,
|
||||||
|
paddingBlock: 5,
|
||||||
|
paddingInline: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
// containerStyle={{
|
||||||
|
// bottom: 0,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<Grid.Col
|
||||||
|
span={rightComponent ? 6 : 12}
|
||||||
|
style={{ justifyContent: "center" }}
|
||||||
>
|
>
|
||||||
<Grid>
|
|
||||||
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,15 +4,15 @@ import Grid from "../Grid/GridCustom";
|
|||||||
export default function GridTwoView({
|
export default function GridTwoView({
|
||||||
spanLeft = 6,
|
spanLeft = 6,
|
||||||
spanRight = 6,
|
spanRight = 6,
|
||||||
leftIcon,
|
leftItem,
|
||||||
rightIcon,
|
rightItem,
|
||||||
styleLeft,
|
styleLeft,
|
||||||
styleRight,
|
styleRight,
|
||||||
}: {
|
}: {
|
||||||
spanLeft?: number;
|
spanLeft?: number;
|
||||||
spanRight?: number;
|
spanRight?: number;
|
||||||
leftIcon?: React.ReactNode;
|
leftItem?: React.ReactNode;
|
||||||
rightIcon?: React.ReactNode;
|
rightItem?: React.ReactNode;
|
||||||
styleLeft?: ViewStyle;
|
styleLeft?: ViewStyle;
|
||||||
styleRight?: ViewStyle;
|
styleRight?: ViewStyle;
|
||||||
}) {
|
}) {
|
||||||
@@ -24,13 +24,13 @@ export default function GridTwoView({
|
|||||||
span={spanLeft}
|
span={spanLeft}
|
||||||
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
|
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
|
||||||
>
|
>
|
||||||
{leftIcon}
|
{leftItem}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col
|
<Grid.Col
|
||||||
span={spanRight}
|
span={spanRight}
|
||||||
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
|
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
|
||||||
>
|
>
|
||||||
{rightIcon}
|
{rightItem}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
28
docs/CHANGE_LOG.md
Normal file
28
docs/CHANGE_LOG.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# CHANGE LOG - fixed-admin/18-feb-26
|
||||||
|
|
||||||
|
## Perubahan Tampilan Admin
|
||||||
|
|
||||||
|
### File Baru (4)
|
||||||
|
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
|
||||||
|
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
|
||||||
|
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
|
||||||
|
- `screens/Admin/Voting/BoxVotingStatus.tsx`
|
||||||
|
|
||||||
|
### File Diubah (3)
|
||||||
|
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
|
||||||
|
- `app/(application)/admin/voting/history.tsx` → 5 baris
|
||||||
|
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris
|
||||||
|
|
||||||
|
### API Updates (2)
|
||||||
|
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
|
||||||
|
- `service/api-admin/api-master-admin.ts` → tambah param `page`
|
||||||
|
|
||||||
|
## Fitur Baru
|
||||||
|
- Pagination (infinite scroll)
|
||||||
|
- Pull-to-Refresh
|
||||||
|
- Skeleton Loading
|
||||||
|
- Empty State
|
||||||
|
- Search Functionality
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
+305 baris, -531 baris (net: -226)
|
||||||
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,13 +1,13 @@
|
|||||||
<!-- ===================== Start Penerapan Pagination ===================== -->
|
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
|
||||||
|
|
||||||
File source: app/(application)/(user)/donation/(tabs)/status.tsx
|
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
|
||||||
Folder tujuan: screens/Donation
|
Folder tujuan: screens/Donation
|
||||||
Nama file utama: ScreenStatus.tsx
|
Nama file utama: ScreenFundDisbursement.tsx
|
||||||
|
|
||||||
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenStatus" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
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"
|
Selanjutnya terapkan pagination pada file "Nama file utama"
|
||||||
|
|
||||||
Function fecth: apiDonationGetByStatus
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
File function fetch: service/api-client/api-donation.ts
|
File function fetch: service/api-client/api-donation.ts
|
||||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
@@ -22,15 +22,15 @@ 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 -->
|
<!-- Additional Prompt -->
|
||||||
File refrensi: screens/Event/ScreenStatus.tsx
|
File refrensi: screens/Admin/Event/ScreenEventStatus.tsx
|
||||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
|
||||||
|
|
||||||
<!-- ===================== End Penerapan Pagination ===================== -->
|
<!-- ===================== End Penerapan Pagination ` ===================== -->
|
||||||
|
|
||||||
<!-- ===================== Start Penerapan NewWrapper ===================== -->
|
<!-- ===================== Start Penerapan NewWrapper & Pagination ===================== -->
|
||||||
File utama: screens/Invesment/ScreenTransaction.tsx
|
File utama: screens/Donation/ScreenFundDisbursement.tsx
|
||||||
Function fecth: apiInvestmentGetInvoice
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
File function fetch: service/api-client/api-investment.ts
|
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"
|
||||||
@@ -43,17 +43,75 @@ 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.
|
||||||
|
|
||||||
<!-- ===================== End Penerapan NewWrapper ===================== -->
|
|
||||||
|
<!-- Additinal prompt -->
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
|
||||||
|
|
||||||
<!-- Start Penerapan NewWrapper -->
|
<!-- Start Penerapan NewWrapper -->
|
||||||
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
||||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||||
<!-- End Penerapan NewWrapper -->
|
<!-- End Penerapan NewWrapper -->
|
||||||
|
|
||||||
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
|
<!-- START Prompt Admin Refactoring -->
|
||||||
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
|
<!-- Pindah kode ke Screen Component -->
|
||||||
jika tidak maka terapkan sesuai dengan logika yang sudah ada
|
File source: app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx
|
||||||
|
Folder tujuan: screens/Admin/Donation
|
||||||
|
Nama file utama: ScreenDonationListDisbursementOfFunds.tsx
|
||||||
|
Nama function utama: Admin_ScreenDonationListDisbursementOfFunds
|
||||||
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
|
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"
|
||||||
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.
|
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
|
||||||
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 ?
|
|
||||||
|
|
||||||
|
<!-- Penerapan Pagination -->
|
||||||
|
Function fecth: apiAdminDonationDisbursementOfFundsListById
|
||||||
|
File function fetch: service/api-admin/api-admin-donation.ts
|
||||||
|
|
||||||
|
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" ( string )
|
||||||
|
Kemudian rapikan code nya pisah komponen seperti render item dan lainnya agar lebih rapi dan di dalam return panggil komponen tersebut
|
||||||
|
|
||||||
|
|
||||||
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||||
|
<!-- END Prompt Admin Refactoring -->
|
||||||
|
|
||||||
|
<!-- Additional -->
|
||||||
|
File refrensi: screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
|
||||||
|
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
|
||||||
|
|
||||||
|
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationCategory.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfDonatur.tsx
|
||||||
|
|
||||||
|
<!-- Use Prompt Now -->
|
||||||
|
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
|
||||||
|
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
|
Function fecth: apiAdminMasterBank
|
||||||
|
File function fetch: service/api-admin/api-master-admin.ts
|
||||||
|
|
||||||
|
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" ( string )
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Create FlatList -->
|
||||||
|
File Utama: screens/Admin/App-Information/InformationBankSection.tsx
|
||||||
|
Terapkan FlatList dan pagination pada file "File Utama"
|
||||||
|
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||||
|
Function fecth: apiAdminMasterBank
|
||||||
|
File function fetch: service/api-admin/api-master-admin.ts
|
||||||
|
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
|
||||||
|
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
|
<!-- Create Box -->
|
||||||
|
File Utama: screens/Admin/Donation/Admin_ScreenDonationStatus.tsx
|
||||||
|
Folder tujuan: screens/Admin/Donation
|
||||||
|
Buat box component baru pada file "File Utama" di bagian renderItem,
|
||||||
|
|
||||||
|
<!-- END Create Box -->
|
||||||
|
|
||||||
|
<!-- END Use Prompt Now -->
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20</string>
|
<string>21</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"expo-dev-client": "~6.0.12",
|
"expo-dev-client": "~6.0.12",
|
||||||
"expo-device": "^8.0.9",
|
"expo-device": "^8.0.9",
|
||||||
"expo-document-picker": "~14.0.7",
|
"expo-document-picker": "~14.0.7",
|
||||||
"expo-file-system": "^19.0.15",
|
"expo-file-system": "^19.0.21",
|
||||||
"expo-font": "~14.0.8",
|
"expo-font": "~14.0.8",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.8",
|
"expo-image": "~3.0.8",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"moti": "^0.30.0",
|
"moti": "^0.30.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.4",
|
"react-native": "0.81.5",
|
||||||
"react-native-dotenv": "^3.4.11",
|
"react-native-dotenv": "^3.4.11",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-international-phone-number": "^0.9.3",
|
"react-native-international-phone-number": "^0.9.3",
|
||||||
|
|||||||
@@ -1,114 +1,56 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
BadgeCustom,
|
||||||
CenterCustom,
|
CenterCustom,
|
||||||
Grid,
|
Grid,
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { AccentColor } from "@/constants/color-palet";
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
import { router } from "expo-router";
|
||||||
import { apiAdminMasterBusinessField } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { FontAwesome5 } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminAppInformation_BusinessFieldSection() {
|
interface Bidang {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
item: {
|
||||||
const [loadData, setLoadData] = useState(false);
|
id: string;
|
||||||
|
name: string;
|
||||||
useFocusEffect(
|
slug: string;
|
||||||
useCallback(() => {
|
active: boolean;
|
||||||
onLoadList();
|
createdAt: string;
|
||||||
}, [])
|
updatedAt: string;
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminMasterBusinessField();
|
|
||||||
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR LIST BUSINESS FIELD]", error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminAppInformation_BusinessFieldSection({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: any;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackCustom>
|
<AdminBasicBox
|
||||||
<Grid>
|
onPress={() =>
|
||||||
<Grid.Col span={2} style={{ alignItems: "center" }}>
|
router.push(`/admin/app-information/business-field/${item.item.id}`)
|
||||||
<TextCustom bold>Aksi</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "center" }}>
|
|
||||||
<TextCustom bold>Status</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold>Nama Bidang Bisnis</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
|
||||||
) : (
|
|
||||||
<StackCustom>
|
|
||||||
{listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={2} style={{ alignItems: "center" }}>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<FontAwesome5
|
|
||||||
name="edit"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
onPress={() => {
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${item.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={4}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8} style={{ alignSelf: "center" }}>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
{item?.item?.name || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
<CenterCustom>
|
<CenterCustom>
|
||||||
<BadgeCustom
|
{item?.item?.active ? (
|
||||||
color={
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
item.active ? AccentColor.blue : AccentColor.blackgray
|
) : (
|
||||||
}
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
>
|
)}
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
</CenterCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
<TextCustom>{item.name}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</View>
|
</AdminBasicBox>
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +1,59 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
BadgeCustom,
|
||||||
CenterCustom,
|
CenterCustom,
|
||||||
Grid,
|
Grid,
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { AccentColor } from "@/constants/color-palet";
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
import { router } from "expo-router";
|
||||||
import { apiAdminMasterBank } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { FontAwesome5 } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminAppInformation_Bank() {
|
interface BankProps {
|
||||||
const [listData, setListData] = useState<any | null>(null);
|
item: {
|
||||||
const [loadData, setLoadData] = useState(false);
|
id: string;
|
||||||
|
namaBank: string;
|
||||||
useFocusEffect(
|
namaAkun: string;
|
||||||
useCallback(() => {
|
norek: string;
|
||||||
loadMasterBank();
|
isActive: boolean;
|
||||||
}, [])
|
createdAt: string;
|
||||||
);
|
updatedAt: string;
|
||||||
|
|
||||||
const loadMasterBank = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminMasterBank();
|
|
||||||
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR LIST BANK]", error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
export default function AdminAppInformation_Bank({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: BankProps;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackCustom>
|
<AdminBasicBox
|
||||||
<Grid>
|
onPress={() =>
|
||||||
<Grid.Col span={3}>
|
router.push(`/admin/app-information/information-bank/${item.item.id}`)
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={3}>
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Status
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Nama Bank
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
|
||||||
) : (
|
|
||||||
<StackCustom>
|
|
||||||
{listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={3} style={{ alignItems: "center" }}>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<FontAwesome5
|
|
||||||
name="edit"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
onPress={() => {
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
router.push(
|
|
||||||
`/admin/app-information/information-bank/${item.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={3}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
{item?.item?.namaBank || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size={"small"} bold truncate color="gray">
|
||||||
|
{item?.item?.norek || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
<CenterCustom>
|
<CenterCustom>
|
||||||
<BadgeCustom
|
{item?.item?.isActive ? (
|
||||||
color={
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
item.isActive
|
) : (
|
||||||
? AccentColor.blue
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
: AccentColor.blackgray
|
)}
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
</CenterCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
<TextCustom align="center">{item.namaBank}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</View>
|
</AdminBasicBox>
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
153
screens/Admin/App-Information/ScreenAppInformation.tsx
Normal file
153
screens/Admin/App-Information/ScreenAppInformation.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { ScrollableCustom, StackCustom } from "@/components";
|
||||||
|
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||||
|
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 AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
||||||
|
import AdminAppInformation_Bank_Component from "@/screens/Admin/App-Information/InformationBankSection";
|
||||||
|
import { apiFetchAdminMasterAppInformation } from "@/service/api-admin/api-master-admin";
|
||||||
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Alert, RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
export function Admin_ScreenAppInformation() {
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
||||||
|
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiFetchAdminMasterAppInformation({
|
||||||
|
category: activeCategory as string,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [activeCategory],
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch job by status:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
setActivePage(item.label);
|
||||||
|
// tambahkan logika lain seperti filter dsb.
|
||||||
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [activeCategory]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollComponent = (
|
||||||
|
<StackCustom>
|
||||||
|
<ScrollableCustom
|
||||||
|
data={listPage.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as any}
|
||||||
|
/>
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title={activePage}
|
||||||
|
rightComponent={
|
||||||
|
<AdminActionIconPlus
|
||||||
|
onPress={() => {
|
||||||
|
if (activeCategory === "bank") {
|
||||||
|
router.push("/admin/app-information/information-bank/create");
|
||||||
|
} else if (activeCategory === "business") {
|
||||||
|
router.push("/admin/app-information/business-field/create");
|
||||||
|
} else if (activeCategory === "sticker") {
|
||||||
|
Alert.alert("Coming Soon", "Next Update");
|
||||||
|
// router.push("/admin/app-information/sticker/create");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
);
|
||||||
|
|
||||||
|
// const renderContent = () => {
|
||||||
|
// switch (activeCategory) {
|
||||||
|
// case "bank":
|
||||||
|
// return <AdminAppInformation_Bank_Component />;
|
||||||
|
// case "business":
|
||||||
|
// return <AdminAppInformation_BusinessFieldSection />;
|
||||||
|
// // case "sticker":
|
||||||
|
// // return <AdminAppInformation_StickerSection />;
|
||||||
|
// default:
|
||||||
|
// return <AdminAppInformation_Bank_Component />;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const renderItem = (item: any) => {
|
||||||
|
if (activeCategory === "bank") {
|
||||||
|
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
|
||||||
|
} else if (activeCategory === "business") {
|
||||||
|
return (
|
||||||
|
<AdminAppInformation_BusinessFieldSection key={item.id} item={item} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
headerComponent={scrollComponent}
|
||||||
|
// ListHeaderComponent={
|
||||||
|
|
||||||
|
// }
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
// Data dan render
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={(item: any) => renderItem(item)}
|
||||||
|
/>
|
||||||
|
// {renderContent()}
|
||||||
|
// </NewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPage = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
label: "Informasi Bank",
|
||||||
|
value: "bank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
label: "Bidang & Sub Bidang",
|
||||||
|
value: "business",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "3",
|
||||||
|
// label: "Stiker",
|
||||||
|
// value: "sticker",
|
||||||
|
// },
|
||||||
|
];
|
||||||
185
screens/Admin/App-Information/ScreenBusinessFieldDetail.tsx
Normal file
185
screens/Admin/App-Information/ScreenBusinessFieldDetail.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
BaseBox,
|
||||||
|
CenterCustom,
|
||||||
|
NewWrapper,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
|
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import {
|
||||||
|
ICON_SIZE_SMALL,
|
||||||
|
PAGINATION_DEFAULT_TAKE,
|
||||||
|
} from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export function Admin_ScreenBusinessFieldDetail() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const [bidang, setBidang] = useState<any | null>(null);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiAdminMasterBusinessFieldById({
|
||||||
|
category: "only-sub-bidang",
|
||||||
|
id: id as any,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
// Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [id],
|
||||||
|
onError: (error) => {
|
||||||
|
console.log("Error fetching data sub bidang", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: "",
|
||||||
|
emptyMessage: "Tidak ada data pengguna",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
onLoadBidang();
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [id]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLoadBidang = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiAdminMasterBusinessFieldById({
|
||||||
|
id: id as string,
|
||||||
|
category: "all",
|
||||||
|
});
|
||||||
|
setBidang(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
setBidang(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const renderHeader = () => (
|
||||||
|
<View>
|
||||||
|
<BaseBox
|
||||||
|
onPress={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/app-information/business-field/${id}/bidang-update`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<GridSpan_NewComponent
|
||||||
|
span1={10}
|
||||||
|
span2={2}
|
||||||
|
text1={
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom bold size={"large"}>
|
||||||
|
{bidang?.bidang?.name}
|
||||||
|
</TextCustom>
|
||||||
|
{bidang?.bidang.active ? (
|
||||||
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</StackCustom>
|
||||||
|
}
|
||||||
|
text2={
|
||||||
|
<CenterCustom>
|
||||||
|
<Ionicons
|
||||||
|
name="caret-forward"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</CenterCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
|
||||||
|
<CenterCustom>
|
||||||
|
<TextCustom bold>Sub Bidang</TextCustom>
|
||||||
|
</CenterCustom>
|
||||||
|
<Spacing height={5} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: any }) => (
|
||||||
|
<BaseBox
|
||||||
|
onPress={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<GridSpan_NewComponent
|
||||||
|
span1={10}
|
||||||
|
span2={2}
|
||||||
|
text1={
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom bold size={"large"}>
|
||||||
|
{item.name}
|
||||||
|
</TextCustom>
|
||||||
|
{item?.isActive ? (
|
||||||
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</StackCustom>
|
||||||
|
}
|
||||||
|
text2={
|
||||||
|
<CenterCustom>
|
||||||
|
<Ionicons
|
||||||
|
name="caret-forward"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</CenterCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
headerComponent={
|
||||||
|
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
||||||
|
}
|
||||||
|
ListHeaderComponent={renderHeader()}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
screens/Admin/Donation/BoxDonationCategory.tsx
Normal file
36
screens/Admin/Donation/BoxDonationCategory.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { TextCustom, BadgeCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
export default function Admin_BoxDonationCategory({item}: {item: any}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminBasicBox
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/donation/category-update?id=${item.id}`);
|
||||||
|
}}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
|
<GridTwoView
|
||||||
|
leftItem={<TextCustom bold>{item?.name || "-"}</TextCustom>}
|
||||||
|
rightItem={
|
||||||
|
<View>
|
||||||
|
{item?.active ? (
|
||||||
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
spanLeft={8}
|
||||||
|
spanRight={4}
|
||||||
|
styleRight={{
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AdminBasicBox>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Divider, StackCustom, TextCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
interface BoxDonationListDisbursementOfFundsProps {
|
||||||
|
item: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin_BoxDonationListDisbursementOfFunds({
|
||||||
|
item,
|
||||||
|
}: BoxDonationListDisbursementOfFundsProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(
|
||||||
|
`/admin/donation/${item?.id}/detail-disbursement-of-funds`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<View style={{ paddingBlock: 8 }}>
|
||||||
|
<TextCustom size={"large"} bold truncate>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
<Divider />
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Tanggal</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{dayjs(item?.createdAt).format("DD-MM-YYYY") || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Nominal</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom bold>
|
||||||
|
Rp {formatCurrencyDisplay(item?.nominalCair)}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
screens/Admin/Donation/BoxDonationListOfDonatur.tsx
Normal file
68
screens/Admin/Donation/BoxDonationListOfDonatur.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
Divider,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom
|
||||||
|
} from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
interface BoxDonationListOfDonaturProps {
|
||||||
|
item: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin_BoxDonationListOfDonatur({
|
||||||
|
item,
|
||||||
|
}: BoxDonationListOfDonaturProps) {
|
||||||
|
const statusName = item?.DonasiMaster_StatusInvoice?.name || "-";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(
|
||||||
|
`/admin/donation/${item?.id}/${_.lowerCase(
|
||||||
|
item?.DonasiMaster_StatusInvoice?.name,
|
||||||
|
)}/transaction-detail`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<View style={{ paddingBlock: 8 }}>
|
||||||
|
<TextCustom size={"large"} bold truncate>
|
||||||
|
{item?.Author?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
<Divider />
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Status</TextCustom>}
|
||||||
|
value={
|
||||||
|
<BadgeCustom
|
||||||
|
color={colorBadgeTransaction({
|
||||||
|
status: statusName,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{statusName}
|
||||||
|
</BadgeCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Nominal</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{item?.nominal
|
||||||
|
? `Rp ${formatCurrencyDisplay(item?.nominal)}`
|
||||||
|
: "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
screens/Admin/Donation/BoxDonationStatus.tsx
Normal file
54
screens/Admin/Donation/BoxDonationStatus.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Divider, StackCustom, TextCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
interface BoxDonationStatusProps {
|
||||||
|
item: any;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin_BoxDonationStatus({
|
||||||
|
item,
|
||||||
|
status,
|
||||||
|
}: BoxDonationStatusProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/donation/${item.id}/${status}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<View style={{ paddingBlock: 8 }}>
|
||||||
|
<TextCustom size={"large"} bold truncate={2}>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
<Divider />
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Durasi</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{item?.DonasiMaster_Durasi?.name || "-"} hari
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Target</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{item?.target
|
||||||
|
? `Rp ${formatCurrencyDisplay(item?.target)}`
|
||||||
|
: "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
screens/Admin/Donation/ScreenDonationCategory.tsx
Normal file
107
screens/Admin/Donation/ScreenDonationCategory.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { BadgeCustom, TextCustom } from "@/components";
|
||||||
|
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
|
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 { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
|
||||||
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
import Admin_BoxDonationCategory from "./BoxDonationCategory";
|
||||||
|
|
||||||
|
export function Admin_ScreenDonationCategory() {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminMasterDonationCategory({
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen action plus untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminActionIconPlus
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/donation/category-create`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component untuk title
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title="Kategori Donasi"
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar kategori donasi
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxDonationCategory key={index} item={item} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
|
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 { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Admin_BoxDonationListDisbursementOfFunds from "./BoxDonationListDisbursementOfFunds";
|
||||||
|
|
||||||
|
export function Admin_ScreenDonationListDisbursementOfFunds() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminDonationDisbursementOfFundsListById({
|
||||||
|
id: id as string,
|
||||||
|
category: "get-all",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: "",
|
||||||
|
dependencies: [id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header component dengan back button dan title
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => <AdminBackButtonAntTitle title="Daftar Pencairan Dana" />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar pencairan dana
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxDonationListDisbursementOfFunds key={index} item={item} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: "",
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
screens/Admin/Donation/ScreenDonationListOfDonatur.tsx
Normal file
134
screens/Admin/Donation/ScreenDonationListOfDonatur.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { SelectCustom } from "@/components";
|
||||||
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
|
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 { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
|
||||||
|
import { apiMasterTransaction } from "@/service/api-client/api-master";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
import Admin_BoxDonationListOfDonatur from "./BoxDonationListOfDonatur";
|
||||||
|
|
||||||
|
export function Admin_ScreenDonationListOfDonatur() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const [selectValue, setSelectValue] = useState<string | null>(null);
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
|
||||||
|
const [master, setMaster] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminDonationListOfDonatur({
|
||||||
|
id: id as string,
|
||||||
|
status: selectedStatus as any,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: "",
|
||||||
|
dependencies: [id, selectedStatus],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load master data untuk select option
|
||||||
|
useEffect(() => {
|
||||||
|
onLoadMaster();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onLoadMaster = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiMasterTransaction();
|
||||||
|
if (response.success) {
|
||||||
|
setMaster(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
setMaster([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Komponen select untuk filter status
|
||||||
|
const searchComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<View style={{ flexDirection: "row", gap: 5 }}>
|
||||||
|
<SelectCustom
|
||||||
|
placeholder="Pilih status transaksi"
|
||||||
|
data={
|
||||||
|
_.isEmpty(master)
|
||||||
|
? []
|
||||||
|
: master?.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
value={selectValue}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
setSelectValue(value);
|
||||||
|
const nameSelected = master.find((item: any) => item.id === value);
|
||||||
|
const statusChooses = _.lowerCase(nameSelected?.name);
|
||||||
|
setSelectedStatus(statusChooses);
|
||||||
|
}}
|
||||||
|
styleContainer={{ width: "100%", marginBottom: 0 }}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
[master, selectValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component dengan back button dan select filter
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => <AdminBackButtonAntTitle newComponent={searchComponent} />,
|
||||||
|
[searchComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar donatur
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxDonationListOfDonatur key={index} item={item} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: "",
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
screens/Admin/Donation/ScreenDonationStatus.tsx
Normal file
112
screens/Admin/Donation/ScreenDonationStatus.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { ActionIcon, SearchInput, TextCustom } from "@/components";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import {
|
||||||
|
ICON_SIZE_BUTTON,
|
||||||
|
PAGINATION_DEFAULT_TAKE,
|
||||||
|
} from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
|
||||||
|
import { Octicons } from "@expo/vector-icons";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Admin_BoxDonationStatus from "./BoxDonationStatus";
|
||||||
|
|
||||||
|
export function Admin_ScreenDonationStatus() {
|
||||||
|
const { status } = useLocalSearchParams();
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminDonation({
|
||||||
|
category: status as "publish" | "review" | "reject",
|
||||||
|
search: searchQuery,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [status],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen search input untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<SearchInput
|
||||||
|
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||||
|
placeholder="Cari judul donasi"
|
||||||
|
value={search}
|
||||||
|
onChangeText={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar donasi
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxDonationStatus
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
status={status as string}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component dengan judul status donasi
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title={`Donasi ${_.startCase(status as string)}`}
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status, rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
screens/Admin/Event/BoxEventParticipant.tsx
Normal file
55
screens/Admin/Event/BoxEventParticipant.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
BaseBox,
|
||||||
|
Grid,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom
|
||||||
|
} from "@/components";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { View } from "moti";
|
||||||
|
|
||||||
|
interface Admin_BoxEventParticipantProps {
|
||||||
|
item: any;
|
||||||
|
startDate?: dayjs.Dayjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Admin_BoxEventParticipant({
|
||||||
|
item,
|
||||||
|
startDate,
|
||||||
|
}: Admin_BoxEventParticipantProps) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<StackCustom gap={"sm"}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
{item?.User?.username}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||||
|
{startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||||
|
<BadgeCustom
|
||||||
|
style={{ alignSelf: "flex-end" }}
|
||||||
|
color={item?.isPresent ? "green" : "red"}
|
||||||
|
>
|
||||||
|
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||||
|
</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BadgeCustom style={{ alignSelf: "flex-end" }} color="gray">
|
||||||
|
-
|
||||||
|
</BadgeCustom>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
screens/Admin/Event/BoxEventStatus.tsx
Normal file
48
screens/Admin/Event/BoxEventStatus.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { StackCustom, TextCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { Divider } from "react-native-paper";
|
||||||
|
|
||||||
|
interface Admin_BoxEventStatusProps {
|
||||||
|
item: any;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Admin_BoxEventStatus({ item, status }: Admin_BoxEventStatusProps) {
|
||||||
|
return (
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/event/${item.id}/${status}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<View style={{ paddingBlock: 8 }}>
|
||||||
|
<TextCustom size={"large"} bold truncate={2}>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
<Divider />
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Mulai</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.tanggal }) || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Berakhir</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.tanggalSelesai }) || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
screens/Admin/Event/ScreenEventListOfParticipants.tsx
Normal file
83
screens/Admin/Event/ScreenEventListOfParticipants.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
|
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 { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { Admin_BoxEventParticipant } from "./BoxEventParticipant";
|
||||||
|
|
||||||
|
export function Admin_ScreenEventListOfParticipants() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminEventListOfParticipants({
|
||||||
|
id: id as string,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [id],
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error loading participants:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item untuk daftar peserta
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxEventParticipant
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
startDate={dayjs(item?.Event?.tanggal)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: "",
|
||||||
|
emptyMessage: "Belum ada peserta",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id.toString()}
|
||||||
|
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
screens/Admin/Event/ScreenEventStatus.tsx
Normal file
108
screens/Admin/Event/ScreenEventStatus.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { SearchInput } from "@/components";
|
||||||
|
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 { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { Admin_BoxEventStatus } from "./BoxEventStatus";
|
||||||
|
|
||||||
|
export function Admin_ScreenEventStatus() {
|
||||||
|
const { status } = useLocalSearchParams();
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminEvent({
|
||||||
|
category: status as
|
||||||
|
| "publish"
|
||||||
|
| "review"
|
||||||
|
| "history"
|
||||||
|
| "dashboard"
|
||||||
|
| "type-of-event",
|
||||||
|
search: searchQuery,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [status],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen kanan untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<SearchInput
|
||||||
|
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||||
|
placeholder="Cari"
|
||||||
|
value={search}
|
||||||
|
onChangeText={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar event
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxEventStatus key={index} item={item} status={status as string} />
|
||||||
|
),
|
||||||
|
[status],
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title={`Event ${_.startCase(status as string)}`}
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status, rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id.toString()}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
screens/Admin/Job/BoxStatusJob.tsx
Normal file
29
screens/Admin/Job/BoxStatusJob.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Spacing, StackCustom, TextCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { Divider } from "react-native-paper";
|
||||||
|
|
||||||
|
interface BoxStatusJobProps {
|
||||||
|
item: any;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoxStatusJob({ item, status }: BoxStatusJobProps) {
|
||||||
|
return (
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/job/${item.id}/${status}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<View style={{paddingBlock: 8}}>
|
||||||
|
<TextCustom size={"large"} align="center" bold truncate={2}>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
screens/Admin/Job/ScreenJobStatus.tsx
Normal file
103
screens/Admin/Job/ScreenJobStatus.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { SearchInput } from "@/components";
|
||||||
|
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 { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { Divider } from "react-native-paper";
|
||||||
|
import { BoxStatusJob } from "./BoxStatusJob";
|
||||||
|
|
||||||
|
export function Admin_ScreenJobStatus() {
|
||||||
|
const { status } = useLocalSearchParams();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminJob({
|
||||||
|
category: status as "publish" | "review" | "reject",
|
||||||
|
search: searchQuery,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [status],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen kanan untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Cari perkerjaan"
|
||||||
|
onChangeText={setSearch}
|
||||||
|
value={search}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar pekerjaan
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BoxStatusJob key={index} item={item} status={status as string} />
|
||||||
|
),
|
||||||
|
[status],
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title={`Job ${_.startCase(status as string)}`}
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status, rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Tidak ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id.toString()}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
127
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/* 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, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
screens/Admin/Voting/BoxVotingStatus.tsx
Normal file
54
screens/Admin/Voting/BoxVotingStatus.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Divider, StackCustom, TextCustom } from "@/components";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
interface BoxVotingStatusProps {
|
||||||
|
item: any;
|
||||||
|
status?: string;
|
||||||
|
path: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin_BoxVotingStatus({
|
||||||
|
item,
|
||||||
|
status,
|
||||||
|
path,
|
||||||
|
}: BoxVotingStatusProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminBasicBox
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<View style={{ paddingBlock: 8 }}>
|
||||||
|
<TextCustom size={"large"} bold truncate={2}>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
<Divider />
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Mulai</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.awalVote }) || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GridSpan_4_8
|
||||||
|
label={<TextCustom>Berakhir</TextCustom>}
|
||||||
|
value={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.akhirVote }) || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
Normal file
152
screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { BadgeCustom, TextCustom } from "@/components";
|
||||||
|
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||||
|
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
|
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
||||||
|
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 { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
|
||||||
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export function Admin_ScreenEventTypeOfEvent() {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminMasterTypeOfEvent({
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen action plus untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminActionIconPlus
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/event/type-create`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component untuk title
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle title="Tipe Acara Event" rightComponent={rightComponent} />
|
||||||
|
),
|
||||||
|
[rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render header tabel (Aksi, Status, Tipe Acara)
|
||||||
|
const renderTableHeader = useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<GridViewCustomSpan
|
||||||
|
span1={2}
|
||||||
|
span2={5}
|
||||||
|
span3={5}
|
||||||
|
component1={
|
||||||
|
<TextCustom bold align="center">
|
||||||
|
Aksi
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
component2={
|
||||||
|
<TextCustom bold align="center">
|
||||||
|
Status
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
component3={<TextCustom bold>Tipe Acara</TextCustom>}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar tipe event (mengikuti pattern InformationBankSection)
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<AdminBasicBox
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/admin/event/type-update?id=${item.id}`);
|
||||||
|
}}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
|
<GridTwoView
|
||||||
|
leftItem={<TextCustom bold>{item?.name || "-"}</TextCustom>}
|
||||||
|
rightItem={
|
||||||
|
<View>
|
||||||
|
{item?.active ? (
|
||||||
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
spanLeft={8}
|
||||||
|
spanRight={4}
|
||||||
|
styleRight={{
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AdminBasicBox>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
screens/Admin/Voting/ScreenVotingHistory.tsx
Normal file
102
screens/Admin/Voting/ScreenVotingHistory.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { SearchInput } from "@/components";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
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 { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Admin_BoxVotingStatus from "./BoxVotingStatus";
|
||||||
|
|
||||||
|
export function Admin_ScreenVotingHistory() {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminVoting({
|
||||||
|
category: "history",
|
||||||
|
search: searchQuery,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen search input untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<SearchInput
|
||||||
|
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||||
|
placeholder="Cari"
|
||||||
|
value={search}
|
||||||
|
onChangeText={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxVotingStatus
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
path={`/admin/voting/${item.id}/history`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component dengan judul voting history
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle title="Riwayat" rightComponent={rightComponent} />
|
||||||
|
),
|
||||||
|
[rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
screens/Admin/Voting/ScreenVotingStatus.tsx
Normal file
110
screens/Admin/Voting/ScreenVotingStatus.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { SearchInput } from "@/components";
|
||||||
|
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||||
|
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 { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Admin_BoxVotingStatus from "./BoxVotingStatus";
|
||||||
|
|
||||||
|
export function Admin_ScreenVotingStatus() {
|
||||||
|
const { status } = useLocalSearchParams();
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
// Gunakan hook pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiAdminVoting({
|
||||||
|
category: status as any,
|
||||||
|
search: searchQuery,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
} else {
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [status],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen search input untuk header
|
||||||
|
const rightComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<SearchInput
|
||||||
|
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||||
|
placeholder="Cari"
|
||||||
|
value={search}
|
||||||
|
onChangeText={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render item untuk daftar voting
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Admin_BoxVotingStatus
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
status={status as string}
|
||||||
|
path={`/admin/voting/${item.id}/${status}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header component dengan judul status voting
|
||||||
|
const headerComponent = useMemo(
|
||||||
|
() => (
|
||||||
|
<AdminComp_BoxTitle
|
||||||
|
title={`Voting ${_.startCase(status as string)}`}
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status, rightComponent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buat komponen-komponen pagination
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
searchQuery: search,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor="#E1B525"
|
||||||
|
colors={["#E1B525"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import { ScrollableCustom } from "@/components";
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
TextCustom,
|
|
||||||
} from "@/components";
|
|
||||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
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 { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||||
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
||||||
import { usePagination } from "@/hooks/use-pagination";
|
|
||||||
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
||||||
import { useFocusEffect } from "expo-router";
|
import { useFocusEffect } 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 { createPaginationComponents } from "@/helpers/paginationHelpers";
|
|
||||||
|
|
||||||
interface DonationStatusProps {
|
interface DonationStatusProps {
|
||||||
initialStatus?: string;
|
initialStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Donation_ScreenStatus({ initialStatus = "publish" }: DonationStatusProps) {
|
export default function Donation_ScreenStatus({
|
||||||
|
initialStatus = "publish",
|
||||||
|
}: DonationStatusProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(initialStatus);
|
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||||
|
initialStatus,
|
||||||
|
);
|
||||||
|
|
||||||
const pagination = usePagination({
|
const pagination = usePagination({
|
||||||
fetchFunction: async (page) => {
|
fetchFunction: async (page) => {
|
||||||
@@ -32,18 +33,19 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
|
|||||||
page: String(page),
|
page: String(page),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
pageSize: 5, // Sesuaikan dengan jumlah item per halaman dari API
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
dependencies: [user?.id, activeCategory],
|
dependencies: [user?.id, activeCategory],
|
||||||
});
|
});
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
pagination.onRefresh();
|
pagination.onRefresh();
|
||||||
}, [user?.id, activeCategory])
|
}, [user?.id, activeCategory]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
const handlePress = (item: any) => {
|
||||||
setActiveCategory(item.value);
|
setActiveCategory(item.value);
|
||||||
|
pagination.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollComponent = (
|
const scrollComponent = (
|
||||||
@@ -59,20 +61,18 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
<Donasi_BoxStatus
|
<Donasi_BoxStatus data={item} status={activeCategory as string} />
|
||||||
data={item}
|
|
||||||
status={activeCategory as string}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
loading: pagination.loading,
|
loading: pagination.loading,
|
||||||
refreshing: pagination.refreshing,
|
refreshing: pagination.refreshing,
|
||||||
listData: pagination.listData,
|
listData: pagination.listData,
|
||||||
isInitialLoad: pagination.isInitialLoad,
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
emptyMessage: `Tidak ada data ${activeCategory}`,
|
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||||
skeletonCount: 5,
|
skeletonCount: 5,
|
||||||
skeletonHeight: 200,
|
skeletonHeight: 120,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,6 +86,8 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={pagination.refreshing}
|
refreshing={pagination.refreshing}
|
||||||
onRefresh={pagination.onRefresh}
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
hideFooter
|
hideFooter
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ViewWrapper,
|
ButtonCustom,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
DummyLandscapeImage,
|
DummyLandscapeImage,
|
||||||
|
Grid,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
Grid,
|
ViewWrapper,
|
||||||
ButtonCustom,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
@@ -15,8 +15,8 @@ import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
|||||||
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
||||||
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useFocusEffect, router } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { useState, useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import MapView, { Marker } from "react-native-maps";
|
import MapView, { Marker } from "react-native-maps";
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export default function MapsView() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
handlerLoadList();
|
handlerLoadList();
|
||||||
}, [])
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
const handlerLoadList = async () => {
|
||||||
@@ -146,52 +146,52 @@ export default function MapsView() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="building-o"
|
name="building-o"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
|
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="list-outline"
|
name="list-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="call-outline"
|
name="call-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.nomorTelepon}</TextCustom>}
|
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { apiConfig } from "../api-config";
|
|||||||
export async function apiAdminDonation({
|
export async function apiAdminDonation({
|
||||||
category,
|
category,
|
||||||
search,
|
search,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
category: "dashboard" | "publish" | "review" | "reject";
|
category: "dashboard" | "publish" | "review" | "reject";
|
||||||
search?: string;
|
search?: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/donation?category=${category}&search=${search}`
|
`/mobile/admin/donation?category=${category}&search=${search}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -52,15 +54,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 +110,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,
|
||||||
@@ -141,12 +133,16 @@ export async function apiAdminDonationDisbursementOfFundsCreated({
|
|||||||
export async function apiAdminDonationDisbursementOfFundsListById({
|
export async function apiAdminDonationDisbursementOfFundsListById({
|
||||||
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(`/mobile/admin/donation/${id}/disbursement?category=${category}`);
|
const response = await apiConfig.get(
|
||||||
|
`/mobile/admin/donation/${id}/disbursement?category=${category}&page=${page}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
|
|||||||
export async function apiAdminEvent({
|
export async function apiAdminEvent({
|
||||||
category,
|
category,
|
||||||
search,
|
search,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
category: "dashboard" | "history" | "publish" | "review" | "type-of-event";
|
category: "dashboard" | "history" | "publish" | "review" | "type-of-event";
|
||||||
search?: string;
|
search?: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/event?category=${category}&search=${search}`
|
`/mobile/admin/event?category=${category}&search=${search}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -48,10 +50,18 @@ export async function apiAdminEventUpdateStatus({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiAdminEventListOfParticipants({ id }: { id: string }) {
|
export async function apiAdminEventListOfParticipants({
|
||||||
|
id,
|
||||||
|
page = "1",
|
||||||
|
search = ""
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
page?: string;
|
||||||
|
search?: string;
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/event/${id}/participants`
|
`/mobile/admin/event/${id}/participants?page=${page}&search=${search}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
|
|||||||
export async function apiAdminJob({
|
export async function apiAdminJob({
|
||||||
category,
|
category,
|
||||||
search,
|
search,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
category: "dashboard" | "publish" | "review" | "reject";
|
category: "dashboard" | "publish" | "review" | "reject";
|
||||||
search?: string;
|
search?: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/job?category=${category}&search=${search}`
|
`/mobile/admin/job?category=${category}&search=${search}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
`/mobile/admin/user/${id}?category=${category}`,
|
||||||
|
{
|
||||||
data: {
|
data: {
|
||||||
active,
|
active,
|
||||||
role,
|
role,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { apiConfig } from "../api-config";
|
|||||||
export async function apiAdminVoting({
|
export async function apiAdminVoting({
|
||||||
category,
|
category,
|
||||||
search,
|
search,
|
||||||
|
page = "1",
|
||||||
}: {
|
}: {
|
||||||
category: "dashboard" | "history" | "publish" | "review" | "report";
|
category: "dashboard" | "history" | "publish" | "review" | "report";
|
||||||
search?: string;
|
search?: string;
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/voting?category=${category}&search=${search}`
|
`/mobile/admin/voting?category=${category}&search=${search}&page=${page}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { apiConfig } from "../api-config";
|
import { apiConfig } from "../api-config";
|
||||||
|
|
||||||
// ================== START MASTER BANK ================== //
|
// ================== START MASTER BANK ================== //
|
||||||
export async function apiAdminMasterBank() {
|
export async function apiAdminMasterBank({ page = "1" }: { page?: string }) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/admin/master/bank`);
|
const response = await apiConfig.get(
|
||||||
|
`/mobile/admin/master/bank?page=${page}`,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -51,9 +53,15 @@ export async function apiAdminMasterBankCreate({ data }: { data: any }) {
|
|||||||
|
|
||||||
// ================== START BUSINNES FIELD ================== //
|
// ================== START BUSINNES FIELD ================== //
|
||||||
|
|
||||||
export async function apiAdminMasterBusinessField() {
|
export async function apiAdminMasterBusinessField({
|
||||||
|
page = "1",
|
||||||
|
}: {
|
||||||
|
page: string;
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/admin/master/business-field`);
|
const response = await apiConfig.get(
|
||||||
|
`/mobile/admin/master/business-field?page=${page}`,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -64,16 +72,19 @@ export async function apiAdminMasterBusinessFieldById({
|
|||||||
id,
|
id,
|
||||||
subBidangId,
|
subBidangId,
|
||||||
category,
|
category,
|
||||||
|
page = "1"
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
subBidangId?: string | null;
|
subBidangId?: string | null;
|
||||||
category: "bidang" | "sub-bidang" | "all";
|
category: "bidang" | "sub-bidang" | "all" | "only-sub-bidang"
|
||||||
|
page?: string
|
||||||
}) {
|
}) {
|
||||||
const queryCategory = category ? `?category=${category}` : "";
|
const queryCategory = category ? `?category=${category}` : "";
|
||||||
const querySubBidang = subBidangId ? `&subBidangId=${subBidangId}` : "";
|
const querySubBidang = subBidangId ? `&subBidangId=${subBidangId}` : "";
|
||||||
|
const queryPage = page ? `&page=${page}` : "";
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}`
|
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}${queryPage}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -95,7 +106,7 @@ export async function apiAdminMasterBusinessFieldUpdate({
|
|||||||
`/mobile/admin/master/business-field/${id}?category=${category}`,
|
`/mobile/admin/master/business-field/${id}?category=${category}`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -113,7 +124,7 @@ export async function apiAdminMasterBusinessFieldCreate({
|
|||||||
`/mobile/admin/master/business-field`,
|
`/mobile/admin/master/business-field`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -124,9 +135,9 @@ export async function apiAdminMasterBusinessFieldCreate({
|
|||||||
// ================== END BUSINNES FIELD ================== //
|
// ================== END BUSINNES FIELD ================== //
|
||||||
|
|
||||||
// ================== START EVENT ================== //
|
// ================== START EVENT ================== //
|
||||||
export async function apiAdminMasterTypeOfEvent() {
|
export async function apiAdminMasterTypeOfEvent({ page = "1" }: { page?: string }) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/admin/master/type-of-event`);
|
const response = await apiConfig.get(`/mobile/admin/master/type-of-event?page=${page}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -139,7 +150,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
|
|||||||
`/mobile/admin/master/type-of-event`,
|
`/mobile/admin/master/type-of-event`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -150,7 +161,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
|
|||||||
export async function apiAdminMasterTypeOfEventGetOne({ id }: { id: string }) {
|
export async function apiAdminMasterTypeOfEventGetOne({ id }: { id: string }) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(
|
const response = await apiConfig.get(
|
||||||
`/mobile/admin/master/type-of-event/${id}`
|
`/mobile/admin/master/type-of-event/${id}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -170,7 +181,7 @@ export async function apiAdminMasterTypeOfEventUpdate({
|
|||||||
`/mobile/admin/master/type-of-event/${id}`,
|
`/mobile/admin/master/type-of-event/${id}`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -182,9 +193,9 @@ export async function apiAdminMasterTypeOfEventUpdate({
|
|||||||
|
|
||||||
// ================== START DONATION ================== //
|
// ================== START DONATION ================== //
|
||||||
|
|
||||||
export async function apiAdminMasterDonationCategory() {
|
export async function apiAdminMasterDonationCategory({ page = "1" }: { page?: string }) {
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/admin/master/donation`);
|
const response = await apiConfig.get(`/mobile/admin/master/donation?page=${page}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -216,7 +227,7 @@ export async function apiAdminMasterDonationCategoryUpdate({
|
|||||||
`/mobile/admin/master/donation/${id}`,
|
`/mobile/admin/master/donation/${id}`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -240,3 +251,27 @@ export async function apiAdminMasterDonationCategoryCreate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================== END DONATION ================== //
|
// ================== END DONATION ================== //
|
||||||
|
|
||||||
|
// ================== START FECTH APP INFORMATION ================== //
|
||||||
|
|
||||||
|
export async function apiFetchAdminMasterAppInformation({
|
||||||
|
page = "1",
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
page: string;
|
||||||
|
category?: "bank" | "business" | string
|
||||||
|
}) {
|
||||||
|
if (category === "bank") {
|
||||||
|
const response = await apiAdminMasterBank({ page });
|
||||||
|
// TODO: implement bank logic
|
||||||
|
return response;
|
||||||
|
} else if (category === "business") {
|
||||||
|
const response = await apiAdminMasterBusinessField({ page });
|
||||||
|
// TODO: implement business logic
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
throw new Error("Category is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== END FECTH APP INFORMATION ================== //
|
||||||
|
|||||||
@@ -109,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) {
|
||||||
@@ -219,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) {
|
||||||
@@ -261,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;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const GStyles = StyleSheet.create({
|
|||||||
// =============== Main Styles =============== //
|
// =============== Main Styles =============== //
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingInline: PADDING_MEDIUM,
|
paddingInline: PADDING_SMALL,
|
||||||
paddingTop: PADDING_EXTRA_SMALL,
|
paddingTop: PADDING_EXTRA_SMALL,
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
// paddingBlock: PADDING_EXTRA_SMALL,
|
// paddingBlock: PADDING_EXTRA_SMALL,
|
||||||
|
|||||||
@@ -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