Compare commits
18 Commits
loaddata/3
...
fixed-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| 42fa80c228 | |||
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 | |||
| 5c931b069c | |||
| b2be7be533 | |||
| 2705f96b01 | |||
| 38a6b424e8 | |||
| 83fa277e03 | |||
| c570a19d84 | |||
| 7415c8c8ce | |||
| 72a3d42013 | |||
| d0abd14047 | |||
| 5b2be20469 | |||
| 60177a1087 | |||
| 771ae45f26 | |||
| 41f4a8ac99 | |||
| 48196cd46b |
258
QWEN.md
258
QWEN.md
@@ -1,179 +1,169 @@
|
|||||||
# HIPMI Mobile Application - Development Guide
|
# HIPMI Mobile Application - Development Context
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
HIPMI Badung Connect is a mobile application built with Expo and React Native. It serves as a connection platform for HIPMI (Himpunan Pengusaha Muda Indonesia) Badung members, featuring authentication, user management, and various business-related functionalities.
|
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named "HIPMI Badung Connect" and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
||||||
|
|
||||||
### Key Technologies
|
### Key Technologies
|
||||||
- **Framework**: Expo (v54.0.0) with React Native (0.81.4)
|
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
|
||||||
- **Architecture**: File-based routing with Expo Router
|
|
||||||
- **State Management**: React Context API
|
|
||||||
- **Styling**: React Native components with custom color palettes
|
|
||||||
- **Authentication**: Token-based authentication with OTP verification
|
|
||||||
- **Database**: AsyncStorage for local storage
|
|
||||||
- **Maps**: React Native Maps and Mapbox integration
|
|
||||||
- **Notifications**: Expo Notifications and Firebase Messaging
|
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
|
- **Architecture**: File-based routing with Expo Router
|
||||||
|
- **State Management**: Context API
|
||||||
|
- **UI Components**: React Native Paper, custom components
|
||||||
|
- **Maps Integration**: Mapbox Maps for React Native
|
||||||
|
- **Push Notifications**: React Native Firebase Messaging
|
||||||
|
- **Build System**: Metro bundler
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
```
|
```
|
||||||
hipmi-mobile/
|
hipmi-mobile/
|
||||||
├── app/ # File-based routing structure
|
├── app/ # Main application screens and routing
|
||||||
│ ├── (application)/ # Main application screens
|
│ ├── _layout.tsx # Root layout component
|
||||||
│ │ ├── (file)/ # File management screens
|
│ ├── index.tsx # Entry point (Login screen)
|
||||||
│ │ ├── (image)/ # Image management screens
|
│ └── ...
|
||||||
│ │ ├── (user)/ # User-specific screens
|
|
||||||
│ │ └── admin/ # Admin-specific screens
|
|
||||||
│ ├── _layout.tsx # Root layout wrapper
|
|
||||||
│ ├── index.tsx # Home screen
|
|
||||||
│ ├── eula.tsx # Terms and conditions screen
|
|
||||||
│ ├── register.tsx # Registration screen
|
|
||||||
│ └── verification.tsx # OTP verification screen
|
|
||||||
├── assets/ # Static assets (images, icons)
|
|
||||||
├── components/ # Reusable UI components
|
├── components/ # Reusable UI components
|
||||||
├── constants/ # Configuration constants
|
├── context/ # State management (AuthContext)
|
||||||
├── context/ # React Context providers
|
├── screens/ # Screen components organized by feature
|
||||||
|
│ ├── Admin/ # Admin panel screens
|
||||||
|
│ ├── Authentication/ # Login, registration flows
|
||||||
|
│ ├── Collaboration/ # Collaboration features
|
||||||
|
│ ├── Event/ # Event management
|
||||||
|
│ ├── Forum/ # Forum functionality
|
||||||
|
│ ├── Home/ # Home screen
|
||||||
|
│ ├── Maps/ # Map integration
|
||||||
|
│ ├── Profile/ # User profile
|
||||||
|
│ └── ...
|
||||||
|
├── assets/ # Images, icons, and static assets
|
||||||
|
├── constants/ # Constants and configuration values
|
||||||
├── hooks/ # Custom React hooks
|
├── hooks/ # Custom React hooks
|
||||||
├── screens/ # Screen components
|
├── lib/ # Utility libraries
|
||||||
├── service/ # API services and configurations
|
├── navigation/ # Navigation configuration
|
||||||
|
├── service/ # API services and business logic
|
||||||
├── types/ # TypeScript type definitions
|
├── types/ # TypeScript type definitions
|
||||||
├── app.config.js # Expo configuration
|
└── utils/ # Helper functions
|
||||||
├── package.json # Dependencies and scripts
|
|
||||||
└── ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js (with bun >=1.0.0 as specified in package.json)
|
- Node.js (with bun as the package manager)
|
||||||
- Expo CLI or bun installed globally
|
- Expo CLI
|
||||||
|
- iOS Simulator or Android Emulator (for native builds)
|
||||||
|
|
||||||
### Setup Instructions
|
### Setup and Development
|
||||||
1. **Install dependencies**:
|
|
||||||
|
1. **Install Dependencies**
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
# or if using npm
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Environment Variables**:
|
2. **Run Development Server**
|
||||||
Create a `.env` file with the following variables:
|
|
||||||
```
|
|
||||||
API_BASE_URL=your_api_base_url
|
|
||||||
BASE_URL=your_base_url
|
|
||||||
DEEP_LINK_URL=your_deep_link_url
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Start the development server**:
|
|
||||||
```bash
|
```bash
|
||||||
# Using bun (as specified in package.json)
|
|
||||||
bun run start
|
bun run start
|
||||||
# or using expo directly
|
```
|
||||||
npx expo start
|
Or use the shorthand:
|
||||||
|
```bash
|
||||||
|
bunx expo start
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Platform-specific commands**:
|
3. **Platform-Specific Commands**
|
||||||
|
- iOS: `bun run ios` or `bunx expo start --ios`
|
||||||
|
- Android: `bun run android` or `bunx expo start --android`
|
||||||
|
- Web: `bun run web` or `bunx expo start --web`
|
||||||
|
|
||||||
|
4. **Linting**
|
||||||
```bash
|
```bash
|
||||||
# Android
|
bun run lint
|
||||||
bun run android
|
|
||||||
# iOS
|
|
||||||
bun run ios
|
|
||||||
# Web
|
|
||||||
bun run web
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
The application uses environment variables defined in the app.config.js file:
|
||||||
|
- `API_BASE_URL`: Base URL for API endpoints
|
||||||
|
- `BASE_URL`: Base application URL
|
||||||
|
- `DEEP_LINK_URL`: URL for deep linking functionality
|
||||||
|
|
||||||
### EAS Build Configuration
|
### EAS Build Configuration
|
||||||
The project uses Expo Application Services (EAS) for building and deployment:
|
The project uses Expo Application Services (EAS) for building and deploying:
|
||||||
- Development builds: `eas build --profile development`
|
- Development builds with development client
|
||||||
- Preview builds: `eas build --profile preview`
|
- Preview builds for internal distribution
|
||||||
- Production builds: `eas build --profile production`
|
- Production builds for app stores
|
||||||
|
|
||||||
## Authentication Flow
|
## Features and Functionality
|
||||||
|
|
||||||
The application implements a phone number-based authentication system with OTP verification:
|
The application appears to include several key modules:
|
||||||
|
- **Authentication**: Login, registration, and verification flows
|
||||||
1. **Login**: User enters phone number → OTP sent via SMS
|
- **Admin Panel**: Administrative functions
|
||||||
2. **Verification**: User enters OTP code → Validates and creates session
|
- **Collaboration**: Tools for member collaboration
|
||||||
3. **Registration**: If user doesn't exist, registration flow is triggered
|
- **Events**: Event management and calendar
|
||||||
4. **Terms Agreement**: User must accept terms and conditions
|
- **Forum**: Discussion forums
|
||||||
5. **Session Management**: Tokens stored in AsyncStorage
|
- **Maps**: Location-based services with Mapbox integration
|
||||||
|
- **Donations**: Donation functionality
|
||||||
### Key Authentication Functions
|
- **Job Board**: Employment opportunities
|
||||||
- `loginWithNomor()`: Initiates OTP sending
|
- **Investment**: Investment-related features
|
||||||
- `validateOtp()`: Verifies OTP and creates session
|
- **Voting**: Voting systems
|
||||||
- `registerUser()`: Registers new users
|
- **Portfolio**: Member portfolio showcase
|
||||||
- `logout()`: Clears session and removes tokens
|
- **Notifications**: Push notifications via Firebase
|
||||||
- `acceptedTerms()`: Handles terms acceptance
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### User Management
|
|
||||||
- Phone number-based registration and login
|
|
||||||
- OTP verification system
|
|
||||||
- Terms and conditions agreement
|
|
||||||
- User profile management
|
|
||||||
|
|
||||||
### Business Features
|
|
||||||
- Business field management (admin section)
|
|
||||||
- File and image management capabilities
|
|
||||||
- Location services integration
|
|
||||||
- Push notifications
|
|
||||||
|
|
||||||
### UI Components
|
|
||||||
- Custom color palette with blue/yellow theme
|
|
||||||
- Responsive layouts using SafeAreaView
|
|
||||||
- Toast notifications for user feedback
|
|
||||||
- Bottom tab navigation and drawer navigation
|
|
||||||
|
|
||||||
## Development Conventions
|
## Development Conventions
|
||||||
|
|
||||||
### Naming Conventions
|
### Coding Standards
|
||||||
- Components: PascalCase (e.g., `UserProfile.tsx`)
|
- TypeScript is used throughout the project for type safety
|
||||||
- Functions: camelCase (e.g., `getUserData()`)
|
- Component-based architecture with reusable components
|
||||||
- Constants: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)
|
- Context API for state management
|
||||||
- Files: kebab-case or camelCase for utility files
|
- File-based routing with Expo Router
|
||||||
|
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||||
### Code Organization
|
|
||||||
- Components are organized by feature/functionality
|
|
||||||
- API services are centralized in the `service/` directory
|
|
||||||
- Type definitions are maintained in the `types/` directory
|
|
||||||
- Constants are grouped by category in the `constants/` directory
|
|
||||||
|
|
||||||
### Styling Approach
|
|
||||||
- Color palette defined in `constants/color-palet.ts`
|
|
||||||
- Reusable styles and themes centralized
|
|
||||||
- Responsive design using React Native's flexbox system
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- Linting: `bun run lint` (uses ESLint with Expo config)
|
- Linting is configured with ESLint
|
||||||
- No specific test framework mentioned in package.json
|
- Standard Expo linting configuration is used
|
||||||
|
|
||||||
## Environment Configuration
|
### Security
|
||||||
|
- Firebase is integrated for authentication and messaging
|
||||||
|
- Camera and location permissions are properly configured
|
||||||
|
- Deep linking is secured with app domain associations
|
||||||
|
|
||||||
The application supports multiple environments through:
|
## Key Dependencies
|
||||||
- Environment variables loaded via dotenv
|
|
||||||
- Expo's extra configuration in `app.config.js`
|
|
||||||
- Platform-specific configurations for iOS and Android
|
|
||||||
|
|
||||||
### Supported Platforms
|
### Core Dependencies
|
||||||
- iOS (with tablet support)
|
- `@react-navigation/*`: Navigation solution for React Native
|
||||||
- Android (with adaptive icons)
|
- `@react-native-firebase/*`: Firebase integration for React Native
|
||||||
- Web (static output)
|
- `@rnmapbox/maps`: Mapbox integration for React Native
|
||||||
|
- `expo-router`: File-based routing for Expo applications
|
||||||
|
- `react-native-paper`: Material Design components for React Native
|
||||||
|
- `react-native-toast-message`: Toast notifications
|
||||||
|
- `react-native-otp-entry`: OTP input components
|
||||||
|
- `react-native-qrcode-svg`: QR code generation
|
||||||
|
|
||||||
## Third-party Integrations
|
### Development Dependencies
|
||||||
|
- `@types/*`: TypeScript type definitions
|
||||||
|
- `eslint-config-expo`: Expo-specific ESLint configuration
|
||||||
|
- `typescript`: Type checking
|
||||||
|
|
||||||
- **Firebase**: Authentication, messaging, and analytics
|
## Platform Support
|
||||||
- **Mapbox**: Advanced mapping capabilities
|
|
||||||
- **React Navigation**: Screen navigation and routing
|
|
||||||
- **React Native Paper**: Material Design components
|
|
||||||
- **Axios**: HTTP client for API requests
|
|
||||||
- **Lodash**: Utility functions
|
|
||||||
- **QR Code SVG**: QR code generation
|
|
||||||
|
|
||||||
## Important Configuration Files
|
The application is configured to support:
|
||||||
|
- **iOS**: With tablet support and proper permissions
|
||||||
|
- **Android**: With adaptive icons and intent filters for deep linking
|
||||||
|
- **Web**: Static output configuration for web deployment
|
||||||
|
|
||||||
- `app.config.js`: Expo configuration, app metadata, and plugin setup
|
## Special Configurations
|
||||||
- `eas.json`: EAS build profiles and submission configuration
|
|
||||||
- `tsconfig.json`: TypeScript compiler options
|
### iOS Configuration
|
||||||
- `package.json`: Dependencies, scripts, and project metadata
|
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||||
- `metro.config.js`: Metro bundler configuration
|
- Supports tablets
|
||||||
|
- Google Services integration
|
||||||
|
- Location permission handling
|
||||||
|
- Associated domains for deep linking
|
||||||
|
|
||||||
|
### Android Configuration
|
||||||
|
- Package name: `com.bip.hipmimobileapp`
|
||||||
|
- Adaptive icons
|
||||||
|
- Edge-to-edge display enabled
|
||||||
|
- Intent filters for HTTPS deep linking
|
||||||
|
- Google Services integration
|
||||||
|
|
||||||
|
### Maps Integration
|
||||||
|
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||||
|
|
||||||
|
### Push Notifications
|
||||||
|
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||||
@@ -1,56 +1,9 @@
|
|||||||
import {
|
import Donation_ScreenBeranda from "@/screens/Donation/ScreenBeranda";
|
||||||
FloatingButton,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
|
|
||||||
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationBeranda() {
|
export default function DonationBeranda() {
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetAll({
|
|
||||||
category: "beranda"
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Donation_ScreenBeranda />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/donation/create")} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">Belum ada donasi</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<Donation_BoxPublish data={item} key={index} id={item.id} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,148 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Donation_ScreenMyDonation from "@/screens/Donation/ScreenMyDonation";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
DummyLandscapeImage,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { Href, router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function DonationMyDonation() {
|
export default function DonationMyDonation() {
|
||||||
const { user } = useAuth();
|
return <Donation_ScreenMyDonation />;
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
if (!user?.id) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Load data gagal, user tidak ditemukan",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetAll({
|
|
||||||
category: "my-donation",
|
|
||||||
authorId: user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlerColor = (status: string) => {
|
|
||||||
if (status === "menunggu") {
|
|
||||||
return "orange";
|
|
||||||
} else if (status === "proses") {
|
|
||||||
return "white";
|
|
||||||
} else if (status === "berhasil") {
|
|
||||||
return "green";
|
|
||||||
} else if (status === "gagal") {
|
|
||||||
return "red";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = ({
|
|
||||||
invoiceId,
|
|
||||||
donationId,
|
|
||||||
status,
|
|
||||||
}: {
|
|
||||||
invoiceId: string;
|
|
||||||
donationId: string;
|
|
||||||
status: string;
|
|
||||||
}) => {
|
|
||||||
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
|
|
||||||
if (status === "menunggu") {
|
|
||||||
router.push(`${url}/invoice`);
|
|
||||||
} else if (status === "proses") {
|
|
||||||
router.push(`${url}/process`);
|
|
||||||
} else if (status === "berhasil") {
|
|
||||||
router.push(`${url}/success`);
|
|
||||||
} else if (status === "gagal") {
|
|
||||||
router.push(`${url}/failed`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewWrapper hideFooter>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada transaksi
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item, index) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingTop={7}
|
|
||||||
paddingBottom={7}
|
|
||||||
onPress={() => {
|
|
||||||
handlePress({
|
|
||||||
status: _.lowerCase(item.statusInvoice),
|
|
||||||
invoiceId: item.id,
|
|
||||||
donationId: item.donasiId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={5}>
|
|
||||||
<DummyLandscapeImage
|
|
||||||
height={100}
|
|
||||||
unClickPath
|
|
||||||
imageId={item.imageId}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={1}>
|
|
||||||
<View />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom truncate={2} bold>
|
|
||||||
{item.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(item.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<BadgeCustom
|
|
||||||
variant="light"
|
|
||||||
color={handlerColor(_.lowerCase(item.statusInvoice))}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{item.statusInvoice}
|
|
||||||
</BadgeCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Donation_ScreenStatus from "@/screens/Donation/ScreenStatus";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
|
||||||
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationStatus() {
|
export default function DonationStatus() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish",
|
|
||||||
);
|
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [activeCategory]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetByStatus({
|
|
||||||
authorId: user?.id as string,
|
|
||||||
status: activeCategory as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListData(null);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter headerComponent={scrollComponent}>
|
<Donation_ScreenStatus initialStatus={status || "publish"} />
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<Donasi_BoxStatus
|
|
||||||
key={index}
|
|
||||||
data={item}
|
|
||||||
status={activeCategory as string}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -31,7 +32,7 @@ export default function DonationEditNews() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [news])
|
}, [news]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -104,7 +105,21 @@ export default function DonationEditNews() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<ViewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!data?.title || !data?.deskripsi}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmitUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||||
<LandscapeFrameUploaded
|
<LandscapeFrameUploaded
|
||||||
@@ -148,15 +163,6 @@ export default function DonationEditNews() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
disabled={!data?.title || !data?.deskripsi}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmitUpdate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
@@ -53,7 +55,7 @@ export default function DonationAddNews() {
|
|||||||
text1: "Gagal menambah berita",
|
text1: "Gagal menambah berita",
|
||||||
});
|
});
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.show({
|
Toast.show({
|
||||||
@@ -70,7 +72,21 @@ export default function DonationAddNews() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!data.title || !data.deskripsi}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||||
<LandscapeFrameUploaded image={image?.uri} />
|
<LandscapeFrameUploaded image={image?.uri} />
|
||||||
@@ -116,17 +132,7 @@ export default function DonationAddNews() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
disabled={!data.title || !data.deskripsi}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
</NewWrapper>
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenListOfNews from "@/screens/Donation/ScreenListOfNews";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
|
||||||
import { formatChatTime } from "@/utils/formatChatTime";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationRecapOfNews() {
|
export default function DonationRecapOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
return <Donation_ScreenListOfNews donationId={id as string} />;
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
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);
|
return <Donation_ScreenRecapOfNews donationId={id as string} />;
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiDonationGetNewsById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Rekap Kabar",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada kabar
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom truncate bold>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom size="small">
|
|
||||||
{formatChatTime(item?.createdAt)}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconPlus />,
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/donation/${id}/(news)/add-news`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
console.log("PATH ", item.path);
|
|
||||||
router.navigate(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
BaseBox,
|
BaseBox,
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
Grid,
|
Grid,
|
||||||
@@ -35,7 +36,7 @@ export default function DonationInvoice() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [invoiceId])
|
}, [invoiceId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -100,7 +101,22 @@ export default function DonationInvoice() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper>
|
<ViewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={!image}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerUpdateInvoice();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<InformationBox
|
<InformationBox
|
||||||
text={`Mohon transfer donasi anda ke rekening dibawah`}
|
text={`Mohon transfer donasi anda ke rekening dibawah`}
|
||||||
@@ -204,16 +220,6 @@ export default function DonationInvoice() {
|
|||||||
</ButtonCenteredOnly>
|
</ButtonCenteredOnly>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
<ButtonCustom
|
|
||||||
disabled={!image}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerUpdateInvoice();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
DotButton,
|
DotButton,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { IconEdit, IconNews } from "@/components/_Icon";
|
import { IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
|
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
|
||||||
@@ -26,18 +27,19 @@ import {
|
|||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
export default function DonasiDetailStatus() {
|
export default function DonasiDetailStatus() {
|
||||||
const { id, status } = useLocalSearchParams();
|
const { id, status } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [data, setData] = useState<any>();
|
const [data, setData] = useState<any | null>(null);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -80,6 +82,17 @@ export default function DonasiDetailStatus() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefresh = useCallback(() => {
|
||||||
|
try {
|
||||||
|
setRefreshing(true);
|
||||||
|
onLoadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error refresh");
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -94,31 +107,50 @@ export default function DonasiDetailStatus() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
<Donation_ComponentBoxDetailData
|
refreshControl={
|
||||||
sisaHari={value.sisa}
|
<RefreshControl
|
||||||
reminder={value.reminder}
|
refreshing={refreshing}
|
||||||
data={data}
|
onRefresh={onRefresh}
|
||||||
bottomSection={
|
tintColor={MainColor.yellow}
|
||||||
status === "publish" && (
|
colors={[MainColor.yellow]}
|
||||||
<Donation_ProgressSection
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!data ? (
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Donation_ComponentBoxDetailData
|
||||||
|
sisaHari={value.sisa}
|
||||||
|
reminder={value.reminder}
|
||||||
|
data={data}
|
||||||
|
showSisaHari={status === "publish" ? true : false}
|
||||||
|
bottomSection={
|
||||||
|
status === "publish" && (
|
||||||
|
<Donation_ProgressSection
|
||||||
|
id={id as string}
|
||||||
|
progres={Number(data?.progres) || 0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Donation_ComponentStoryFunrising
|
||||||
|
id={id as string}
|
||||||
|
dataStory={data?.CeritaDonasi}
|
||||||
|
/>
|
||||||
|
<Spacing />
|
||||||
|
{data && (
|
||||||
|
<Donation_ButtonStatusSection
|
||||||
id={id as string}
|
id={id as string}
|
||||||
progres={Number(data?.progres) || 0}
|
status={status as string}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
/>
|
<Spacing />
|
||||||
<Donation_ComponentStoryFunrising
|
</>
|
||||||
id={id as string}
|
)}
|
||||||
dataStory={data?.CeritaDonasi}
|
</NewWrapper>
|
||||||
/>
|
|
||||||
<Spacing />
|
|
||||||
<Donation_ButtonStatusSection
|
|
||||||
id={id as string}
|
|
||||||
status={status as string}
|
|
||||||
/>
|
|
||||||
<Spacing />
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
isVisible={openDrawer}
|
isVisible={openDrawer}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +63,7 @@ export default function DonationEdit() {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
onLoadList();
|
onLoadList();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -79,7 +82,6 @@ export default function DonationEdit() {
|
|||||||
imageId: response.data.imageId,
|
imageId: response.data.imageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
}
|
}
|
||||||
@@ -182,10 +184,24 @@ export default function DonationEdit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmitUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
||||||
{!data || loadList ? (
|
{!data || loadList ? (
|
||||||
<LoaderCustom />
|
<ListSkeletonComponent />
|
||||||
) : (
|
) : (
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
@@ -260,17 +276,9 @@ export default function DonationEdit() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmitUpdate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenFundDisbursement from "@/screens/Donation/ScreenFundDisbursement";
|
||||||
BaseBox,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
Grid,
|
|
||||||
InformationBox,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import {
|
|
||||||
apiDonationDisbursementOfFundsListById,
|
|
||||||
apiDonationGetOne,
|
|
||||||
} from "@/service/api-client/api-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
export default function DonationFundDisbursement() {
|
export default function DonationFundDisbursement() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
|
|
||||||
const [data, setData] = useState({
|
return <Donation_ScreenFundDisbursement donationId={id as string} />;
|
||||||
totalPencairan: 0,
|
|
||||||
akumulasiPencairan: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
|
|
||||||
const responseData = await apiDonationGetOne({
|
|
||||||
id: id as string,
|
|
||||||
category: "permanent",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseData.success) {
|
|
||||||
setData({
|
|
||||||
totalPencairan: responseData.data.totalPencairan,
|
|
||||||
akumulasiPencairan: responseData.data.akumulasiPencairan,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseList = await apiDonationDisbursementOfFundsListById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseList.success) {
|
|
||||||
setListData(responseList.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
|
|
||||||
<BaseBox>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom size="small">Total Pencairan Dana</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold color="yellow">
|
|
||||||
{data?.akumulasiPencairan} kali
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<StackCustom>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>{item?.title}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom>{dayjs(item?.createdAt).format("DD MMM YYYY")}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<TextCustom>{item?.deskripsi}</TextCustom>
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
onPress={() => {
|
|
||||||
router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`);
|
|
||||||
}}
|
|
||||||
icon="file-text"
|
|
||||||
>
|
|
||||||
Bukti Transaksi
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
DotButton,
|
DotButton,
|
||||||
DrawerCustom,
|
DrawerCustom,
|
||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { IconNews } from "@/components/_Icon";
|
import { IconNews } from "@/components/_Icon";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
|
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
|
||||||
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
|
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
|
||||||
@@ -34,7 +36,7 @@ export default function DonasiDetailBeranda() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -75,10 +77,10 @@ export default function DonasiDetailBeranda() {
|
|||||||
<>
|
<>
|
||||||
<BoxButtonOnFooter>
|
<BoxButtonOnFooter>
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={value?.reminder}
|
disabled={value?.reminder || !data}
|
||||||
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
|
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
|
||||||
>
|
>
|
||||||
{value?.reminder ? "Waktu berakhir" : "Donasi"}
|
{!data ? "Loading..." : value?.reminder ? "Waktu berakhir" : "Donasi"}
|
||||||
</ButtonCustom>
|
</ButtonCustom>
|
||||||
</BoxButtonOnFooter>
|
</BoxButtonOnFooter>
|
||||||
</>
|
</>
|
||||||
@@ -96,21 +98,30 @@ export default function DonasiDetailBeranda() {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper footerComponent={buttonSection}>
|
<NewWrapper footerComponent={buttonSection}>
|
||||||
<StackCustom>
|
{!data ? (
|
||||||
<Donation_ComponentBoxDetailData
|
<CustomSkeleton height={400} />
|
||||||
sisaHari={value.sisa}
|
) : (
|
||||||
reminder={value.reminder}
|
<StackCustom>
|
||||||
data={data}
|
<Donation_ComponentBoxDetailData
|
||||||
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
|
sisaHari={value.sisa}
|
||||||
/>
|
reminder={value.reminder}
|
||||||
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
data={data}
|
||||||
<Donation_ComponentStoryFunrising
|
bottomSection={
|
||||||
id={id as string}
|
<Donation_ProgressSection
|
||||||
dataStory={data?.CeritaDonasi}
|
id={id as string}
|
||||||
/>
|
progres={Number(data?.progres) || 0}
|
||||||
</StackCustom>
|
/>
|
||||||
</ViewWrapper>
|
}
|
||||||
|
/>
|
||||||
|
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
||||||
|
<Donation_ComponentStoryFunrising
|
||||||
|
id={id as string}
|
||||||
|
dataStory={data?.CeritaDonasi}
|
||||||
|
/>
|
||||||
|
</StackCustom>
|
||||||
|
)}
|
||||||
|
</NewWrapper>
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
isVisible={openDrawer}
|
isVisible={openDrawer}
|
||||||
|
|||||||
@@ -1,94 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import {
|
import Donation_ScreenListOfDonatur from "@/screens/Donation/ScreenListOfDonatur";
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { apiAdminDonationListOfDonaturById } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { FontAwesome6 } from "@expo/vector-icons";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function Donation_ListOfDonatur() {
|
export default function DonationListOfDonatur() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
return <Donation_ScreenListOfDonatur donationId={id as string} />;
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationListOfDonaturById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Belum ada donatur
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col
|
|
||||||
span={3}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
|
||||||
<FontAwesome6
|
|
||||||
name="face-smile-wink"
|
|
||||||
size={50}
|
|
||||||
style={{ color: MainColor.yellow }}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={9}>
|
|
||||||
<TextCustom bold size="large">
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
<Spacing/>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
|
|
||||||
<TextCustom bold size="large" color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>
|
|
||||||
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
|
|
||||||
</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -8,8 +9,8 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
@@ -112,7 +113,23 @@ export default function DonationCreateStory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<>
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
|
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
|
||||||
<TextAreaCustom
|
<TextAreaCustom
|
||||||
@@ -166,18 +183,8 @@ export default function DonationCreateStory() {
|
|||||||
value={data.rekening}
|
value={data.rekening}
|
||||||
onChangeText={(value) => setData({ ...data, rekening: value })}
|
onChangeText={(value) => setData({ ...data, rekening: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
@@ -8,8 +9,8 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { apiDonationCreate } from "@/service/api-client/api-donation";
|
import { apiDonationCreate } from "@/service/api-client/api-donation";
|
||||||
import { apiMasterDonation } from "@/service/api-client/api-master";
|
import { apiMasterDonation } from "@/service/api-client/api-master";
|
||||||
@@ -43,7 +44,7 @@ export default function DonationCreate() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadList();
|
onLoadList();
|
||||||
}, [])
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadList = async () => {
|
const onLoadList = async () => {
|
||||||
@@ -125,7 +126,24 @@ export default function DonationCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
footerComponent={
|
||||||
|
<>
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => {
|
||||||
|
handlerSubmit();
|
||||||
|
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Selanjutnya
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
|
||||||
|
|
||||||
@@ -201,20 +219,8 @@ export default function DonationCreate() {
|
|||||||
onChange={(value: any) => setData({ ...data, durasiId: value })}
|
onChange={(value: any) => setData({ ...data, durasiId: value })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmit();
|
|
||||||
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Selanjutnya
|
|
||||||
</ButtonCustom>
|
|
||||||
<Spacing />
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
|
||||||
AvatarUsernameAndOtherComponent,
|
|
||||||
BoxWithHeaderSection,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import {
|
|
||||||
apiEventGetAll
|
|
||||||
} from "@/service/api-client/api-event";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function EventContribution() {
|
export default function EventContribution() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [isLoadList, setIsLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
async function onLoadData() {
|
|
||||||
try {
|
|
||||||
setIsLoadList(true);
|
|
||||||
const response = await apiEventGetAll({
|
|
||||||
category: "contribution",
|
|
||||||
userId: user?.id,
|
|
||||||
});
|
|
||||||
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
|
|
||||||
// const responseListParticipants = await apiEventListOfParticipants({
|
|
||||||
// id: response?.data?.Event?.id,
|
|
||||||
// });
|
|
||||||
// console.log(
|
|
||||||
// "[LIST PARTICIPANTS]",
|
|
||||||
// JSON.stringify(responseListParticipants.data, null, 2)
|
|
||||||
// );
|
|
||||||
// if (responseListParticipants.success) {
|
|
||||||
// setListParticipants(responseListParticipants.data);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadList(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter>
|
<>
|
||||||
{isLoadList ? (
|
<Event_ScreenContribution />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Belum ada kontribusi</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<BoxWithHeaderSection
|
|
||||||
key={index}
|
|
||||||
href={`/event/${item?.Event?.id}/contribution`}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.Event?.Author?.Profile?.imageId}
|
|
||||||
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
|
|
||||||
name={item?.Event?.Author?.username}
|
|
||||||
rightComponent={
|
|
||||||
<TextCustom truncate>
|
|
||||||
{dateTimeView({
|
|
||||||
date: item?.Event?.tanggal,
|
|
||||||
withoutTime: true,
|
|
||||||
})}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextCustom bold align="center" size="xlarge" truncate={2}>
|
|
||||||
{item?.Event?.title}
|
|
||||||
</TextCustom>
|
|
||||||
<Spacing height={0} />
|
|
||||||
|
|
||||||
{/* <Grid>
|
|
||||||
{item?.Event?.Event_Peserta?.map(
|
|
||||||
(item2: any, index2: number) => (
|
|
||||||
<Grid.Col
|
|
||||||
style={{ alignItems: "center" }}
|
|
||||||
span={12 / item?.Event?.Event_Peserta?.length}
|
|
||||||
key={index2}
|
|
||||||
>
|
|
||||||
<AvatarComp
|
|
||||||
size="base"
|
|
||||||
href={`/profile/${item2?.User?.Profile?.id}`}
|
|
||||||
fileId={item2?.User?.Profile?.imageId}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Grid> */}
|
|
||||||
</StackCustom>
|
|
||||||
</BoxWithHeaderSection>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
|
import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
|
||||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
|
||||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function EventHistory() {
|
export default function EventHistory() {
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [isLoadList, setIsLoadList] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoadData({ userId: user?.id });
|
|
||||||
}, [user?.id, activeCategory]);
|
|
||||||
|
|
||||||
async function onLoadData({ userId }: { userId?: string }) {
|
|
||||||
try {
|
|
||||||
setIsLoadList(true);
|
|
||||||
const response = await apiEventGetAll({
|
|
||||||
category: activeCategory === "all" ? "all-history" : "my-history",
|
|
||||||
userId: userId,
|
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadList(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerComponent = (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: 5,
|
|
||||||
backgroundColor: MainColor.soft_darkblue,
|
|
||||||
borderRadius: 50,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonCustom
|
|
||||||
backgroundColor={
|
|
||||||
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
|
|
||||||
}
|
|
||||||
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
|
|
||||||
style={{ width: "49%" }}
|
|
||||||
onPress={() => handlePress("all")}
|
|
||||||
>
|
|
||||||
Semua Riwayat
|
|
||||||
</ButtonCustom>
|
|
||||||
<Spacing width={"2%"} />
|
|
||||||
<ButtonCustom
|
|
||||||
backgroundColor={
|
|
||||||
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
|
|
||||||
}
|
|
||||||
textColor={
|
|
||||||
activeCategory === "main" ? MainColor.black : MainColor.white
|
|
||||||
}
|
|
||||||
style={{ width: "49%" }}
|
|
||||||
onPress={() => handlePress("main")}
|
|
||||||
>
|
|
||||||
Riwayat Saya
|
|
||||||
</ButtonCustom>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper headerComponent={headerComponent} hideFooter>
|
<>
|
||||||
{isLoadList ? (
|
<Event_ScreenHistory />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Belum ada riwayat</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<Event_BoxPublishSection
|
|
||||||
key={index.toString()}
|
|
||||||
data={item}
|
|
||||||
rightComponentAvatar={
|
|
||||||
<TextCustom>
|
|
||||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
href={`/event/${item.id}/history`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,9 @@
|
|||||||
import { LoaderCustom, TextCustom } from "@/components";
|
import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
|
||||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
|
||||||
import FloatingButton from "@/components/Button/FloatingButton";
|
|
||||||
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
|
||||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function EventBeranda() {
|
export default function EventBeranda() {
|
||||||
const [listData, setListData] = useState([]);
|
|
||||||
const [isLoadData, setIsLoadData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoadData(true);
|
|
||||||
const response = await apiEventGetAll({category: "beranda"});
|
|
||||||
// console.log("Response", JSON.stringify(response.data, null, 2));
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Event_ScreenBeranda />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/event/create")} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Belum ada event</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index) => (
|
|
||||||
|
|
||||||
|
|
||||||
<Event_BoxPublishSection
|
|
||||||
key={index}
|
|
||||||
href={`/event/${item.id}/publish`}
|
|
||||||
data={item}
|
|
||||||
rightComponentAvatar={
|
|
||||||
<TextCustom>
|
|
||||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
|
||||||
BoxWithHeaderSection,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
} from "@/components";
|
|
||||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import { apiEventGetByStatus } from "@/service/api-client/api-event";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function EventStatus() {
|
export default function EventStatus() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
|
||||||
|
|
||||||
const id = user?.id || "";
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish"
|
|
||||||
);
|
|
||||||
const [listData, setListData] = useState([]);
|
|
||||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [activeCategory, id])
|
|
||||||
);
|
|
||||||
|
|
||||||
async function onLoadData() {
|
|
||||||
try {
|
|
||||||
setLoadingGetData(true);
|
|
||||||
const response = await apiEventGetByStatus({
|
|
||||||
id: id!,
|
|
||||||
status: activeCategory!,
|
|
||||||
});
|
|
||||||
// console.log("Response", JSON.stringify(response.data, null, 2));
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
setLoadingGetData(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabsComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper headerComponent={tabsComponent}>
|
<>
|
||||||
{loadingGetData ? (
|
<Event_ScreenStatus />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, i) => (
|
|
||||||
<BoxWithHeaderSection
|
|
||||||
key={i}
|
|
||||||
href={`/event/${item.id}/${activeCategory}/detail-event`}
|
|
||||||
>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom truncate bold>
|
|
||||||
{item?.title}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom>
|
|
||||||
{new Date(item?.tanggal).toLocaleDateString()}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</BoxWithHeaderSection>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||||
import {
|
import {
|
||||||
apiEventGetOne,
|
apiEventGetOne,
|
||||||
@@ -48,14 +51,14 @@ export default function EventEdit() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData() {
|
async function onLoadData() {
|
||||||
try {
|
try {
|
||||||
setIsLoadData(true);
|
setIsLoadData(true);
|
||||||
const response = await apiEventGetOne({ id: id as string });
|
const response = await apiEventGetOne({ id: id as string });
|
||||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
setSelectedDate(new Date(response.data.tanggal));
|
setSelectedDate(new Date(response.data.tanggal));
|
||||||
@@ -100,6 +103,15 @@ export default function EventEdit() {
|
|||||||
const startDate = new Date(selectedDate as any);
|
const startDate = new Date(selectedDate as any);
|
||||||
const endDate = new Date(selectedEndDate as any);
|
const endDate = new Date(selectedEndDate as any);
|
||||||
|
|
||||||
|
if (!startDate) {
|
||||||
|
Toast.show({
|
||||||
|
type: "info",
|
||||||
|
text1: "Info",
|
||||||
|
text2: "Tanggal mulai tidak valid",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (startDate >= endDate) {
|
if (startDate >= endDate) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "info",
|
type: "info",
|
||||||
@@ -146,7 +158,7 @@ export default function EventEdit() {
|
|||||||
|
|
||||||
const validateDateRange = (
|
const validateDateRange = (
|
||||||
selectedDate: string | Date,
|
selectedDate: string | Date,
|
||||||
selectedEndDate: string | Date
|
selectedEndDate: string | Date,
|
||||||
): { isValid: boolean; error?: string } => {
|
): { isValid: boolean; error?: string } => {
|
||||||
const startDate = new Date(selectedDate);
|
const startDate = new Date(selectedDate);
|
||||||
const endDate = new Date(selectedEndDate);
|
const endDate = new Date(selectedEndDate);
|
||||||
@@ -174,9 +186,19 @@ export default function EventEdit() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Update"
|
||||||
|
onPress={handlerSubmit}
|
||||||
|
/>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
{isLoadData ? (
|
{isLoadData ? (
|
||||||
<LoaderCustom />
|
<ListSkeletonComponent />
|
||||||
) : (
|
) : (
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
@@ -186,26 +208,15 @@ export default function EventEdit() {
|
|||||||
value={data?.title}
|
value={data?.title}
|
||||||
onChangeText={(value) => setData({ ...data, title: value })}
|
onChangeText={(value) => setData({ ...data, title: value })}
|
||||||
/>
|
/>
|
||||||
<SelectCustom
|
<TextAreaCustom
|
||||||
label="Tipe Event"
|
label="Deskripsi"
|
||||||
placeholder="Pilih tipe event"
|
placeholder="Masukkan deskripsi event"
|
||||||
data={listTypeEvent.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={data?.eventMaster_TipeAcaraId || ""}
|
|
||||||
onChange={(value) => {
|
|
||||||
console.log(value);
|
|
||||||
setData({ ...data, eventMaster_TipeAcaraId: value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextInputCustom
|
|
||||||
label="Lokasi"
|
|
||||||
placeholder="Masukkan lokasi event"
|
|
||||||
required
|
required
|
||||||
value={data?.lokasi}
|
showCount
|
||||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
value={data?.deskripsi}
|
||||||
|
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePickerCustom
|
<DateTimePickerCustom
|
||||||
minimumDate={new Date(Date.now())}
|
minimumDate={new Date(Date.now())}
|
||||||
label="Tanggal & Waktu Mulai"
|
label="Tanggal & Waktu Mulai"
|
||||||
@@ -233,7 +244,7 @@ export default function EventEdit() {
|
|||||||
{
|
{
|
||||||
validateDateRange(
|
validateDateRange(
|
||||||
selectedDate as any,
|
selectedDate as any,
|
||||||
selectedEndDate as any
|
selectedEndDate as any,
|
||||||
).error
|
).error
|
||||||
}
|
}
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
@@ -242,31 +253,37 @@ export default function EventEdit() {
|
|||||||
{
|
{
|
||||||
validateDateRange(
|
validateDateRange(
|
||||||
selectedDate as any,
|
selectedDate as any,
|
||||||
selectedEndDate as any
|
selectedEndDate as any,
|
||||||
).error
|
).error
|
||||||
}
|
}
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
{/* <Spacing /> */}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|
||||||
<TextAreaCustom
|
<SelectCustom
|
||||||
label="Deskripsi"
|
label="Tipe Event"
|
||||||
placeholder="Masukkan deskripsi event"
|
placeholder="Pilih tipe event"
|
||||||
required
|
data={listTypeEvent.map((item: any) => ({
|
||||||
showCount
|
label: item.name,
|
||||||
value={data?.deskripsi}
|
value: item.id,
|
||||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
}))}
|
||||||
|
value={data?.eventMaster_TipeAcaraId || ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
console.log(value);
|
||||||
|
setData({ ...data, eventMaster_TipeAcaraId: value });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<TextInputCustom
|
||||||
<ButtonCustom
|
label="Lokasi"
|
||||||
isLoading={isLoading}
|
placeholder="Masukkan lokasi event"
|
||||||
title="Update"
|
required
|
||||||
onPress={handlerSubmit}
|
value={data?.lokasi}
|
||||||
|
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||||
/>
|
/>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
)}
|
)}
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
|
||||||
AvatarUsernameAndOtherComponent,
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import {
|
|
||||||
apiEventGetOne,
|
|
||||||
apiEventListOfParticipants,
|
|
||||||
} from "@/service/api-client/api-event";
|
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function EventListOfParticipants() {
|
export default function EventListOfParticipants() {
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadtData, setLoadData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
handlerLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlerLoadData = () => {
|
|
||||||
try {
|
|
||||||
onLoadData();
|
|
||||||
onLoadList();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiEventGetOne({ id: id as string });
|
|
||||||
if (response.success) {
|
|
||||||
const date = dayjs(response.data.tanggal);
|
|
||||||
setStartDate(date);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiEventListOfParticipants({ id: id as string });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<>
|
||||||
{loadtData && !listData ? (
|
<Event_ScreenListOfParticipants />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada peserta
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.User?.Profile?.imageId}
|
|
||||||
name={item?.User?.username}
|
|
||||||
avatarHref={`/profile/${item?.User?.Profile?.id}`}
|
|
||||||
rightComponent={
|
|
||||||
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
|
|
||||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BadgeCustom color="gray">-</BadgeCustom>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -100,7 +101,6 @@ export default function EventCreate() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const buttonSubmit = (
|
const buttonSubmit = (
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
@@ -112,7 +112,9 @@ export default function EventCreate() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
placeholder="Masukkan nama event"
|
placeholder="Masukkan nama event"
|
||||||
@@ -121,24 +123,15 @@ export default function EventCreate() {
|
|||||||
onChangeText={(value: any) => setData({ ...data, title: value })}
|
onChangeText={(value: any) => setData({ ...data, title: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectCustom
|
<TextAreaCustom
|
||||||
label="Tipe Event"
|
label="Deskripsi"
|
||||||
placeholder="Pilih tipe event"
|
placeholder="Masukkan deskripsi event"
|
||||||
data={listTypeEvent.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={data?.eventMaster_TipeAcaraId || null}
|
|
||||||
onChange={(value: any) =>
|
|
||||||
setData({ ...data, eventMaster_TipeAcaraId: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInputCustom
|
|
||||||
label="Lokasi"
|
|
||||||
placeholder="Masukkan lokasi event"
|
|
||||||
required
|
required
|
||||||
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
showCount
|
||||||
|
value={data?.deskripsi || ""}
|
||||||
|
onChangeText={(value: any) =>
|
||||||
|
setData({ ...data, deskripsi: value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePickerCustom
|
<DateTimePickerCustom
|
||||||
@@ -168,22 +161,28 @@ export default function EventCreate() {
|
|||||||
</TextCustom>
|
</TextCustom>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
|
<SelectCustom
|
||||||
|
label="Tipe Event"
|
||||||
|
placeholder="Pilih tipe event"
|
||||||
|
data={listTypeEvent.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
|
value={data?.eventMaster_TipeAcaraId || null}
|
||||||
|
onChange={(value: any) =>
|
||||||
|
setData({ ...data, eventMaster_TipeAcaraId: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInputCustom
|
||||||
|
label="Lokasi"
|
||||||
|
placeholder="Masukkan lokasi event"
|
||||||
|
required
|
||||||
|
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
||||||
|
/>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|
||||||
<TextAreaCustom
|
|
||||||
label="Deskripsi"
|
|
||||||
placeholder="Masukkan deskripsi event"
|
|
||||||
required
|
|
||||||
showCount
|
|
||||||
value={data?.deskripsi || ""}
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setData({ ...data, deskripsi: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{buttonSubmit}
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { StackCustom, ViewWrapper } from "@/components";
|
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
@@ -29,14 +29,14 @@ export default function Application() {
|
|||||||
checkVersion();
|
checkVersion();
|
||||||
userData(token as string);
|
userData(token as string);
|
||||||
syncUnreadCount();
|
syncUnreadCount();
|
||||||
}, [user?.id, token])
|
}, [user?.id, token]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData() {
|
async function onLoadData() {
|
||||||
const response = await apiUser(user?.id as string);
|
const response = await apiUser(user?.id as string);
|
||||||
console.log(
|
console.log(
|
||||||
"[Profile ID]>>",
|
"[Profile ID]>>",
|
||||||
JSON.stringify(response?.data?.Profile?.id, null, 2)
|
JSON.stringify(response?.data?.Profile?.id, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
@@ -61,12 +61,29 @@ export default function Application() {
|
|||||||
|
|
||||||
if (data && data?.active === false) {
|
if (data && data?.active === false) {
|
||||||
console.log("User is not active");
|
console.log("User is not active");
|
||||||
return <Redirect href={`/waiting-room`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/waiting-room`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data?.Profile === null) {
|
if (data && data?.Profile === null) {
|
||||||
console.log("Profile is null");
|
console.log("Profile is null");
|
||||||
return <Redirect href={`/profile/create`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/profile/create`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data?.masterUserRoleId !== "1") {
|
||||||
|
console.log("User is not admin");
|
||||||
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/admin/dashboard`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,7 +106,12 @@ export default function Application() {
|
|||||||
/>
|
/>
|
||||||
<ViewWrapper
|
<ViewWrapper
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
footerComponent={
|
footerComponent={
|
||||||
<TabSection
|
<TabSection
|
||||||
|
|||||||
@@ -1,56 +1,9 @@
|
|||||||
import {
|
import Investment_ScreenBursa from "@/screens/Invesment/ScreenBursa";
|
||||||
FloatingButton,
|
|
||||||
LoaderCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import Investment_BoxBerandaSection from "@/screens/Invesment/BoxBerandaSection";
|
|
||||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentBursa() {
|
export default function InvestmentBursa() {
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetAll({
|
|
||||||
category: "bursa"
|
|
||||||
});
|
|
||||||
// console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Investment_ScreenBursa />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/investment/create")} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<Investment_BoxBerandaSection id={item.id} data={item} key={index} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenMyHolding from "@/screens/Invesment/ScreenMyHolding";
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
ProgressCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function InvestmentMyHolding() {
|
export default function InvestmentMyHolding() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [user?.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetAll({
|
|
||||||
category: "my-holding",
|
|
||||||
authorId: user?.id,
|
|
||||||
});
|
|
||||||
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter>
|
<>
|
||||||
{loadingList ? (
|
<Investment_ScreenMyHolding />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
list?.map((item, index) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingTop={7}
|
|
||||||
paddingBottom={7}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
|
||||||
<TextCustom>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
|
|
||||||
<ProgressCustom
|
|
||||||
label={`${item.progress}%`}
|
|
||||||
value={Number(item.progress)}
|
|
||||||
size="lg"
|
|
||||||
animated
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenPortofolio from "@/screens/Invesment/ScreenPortofolio";
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
|
|
||||||
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentPortofolio() {
|
export default function InvestmentPortofolio() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id, activeCategory])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetByStatus({
|
|
||||||
authorId: user?.id as string,
|
|
||||||
status: activeCategory as string,
|
|
||||||
});
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
<>
|
||||||
{loadingList ? (
|
<Investment_ScreenPortofolio />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<Investment_StatusBox
|
|
||||||
key={index}
|
|
||||||
data={item}
|
|
||||||
status={activeCategory as string}
|
|
||||||
href={`/investment/${item.id}/${activeCategory}/detail`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiInvestmentGetInvoice } from "@/service/api-client/api-investment";
|
|
||||||
import { GStyles } from "@/styles/global-styles";
|
|
||||||
import { formatChatTime } from "@/utils/formatChatTime";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function InvestmentTransaction() {
|
export default function InvestmentTransaction() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [list, setList] = useState<any>([]);
|
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [user?.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetInvoice({
|
|
||||||
authorId: user?.id as string,
|
|
||||||
category: "transaction",
|
|
||||||
});
|
|
||||||
console.log("[RESPONSE LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
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 = ({ id, status }: { id: string; status: string }) => {
|
|
||||||
if (status === "menunggu") {
|
|
||||||
router.push(`/investment/${id}/(transaction-flow)/invoice`);
|
|
||||||
} else if (status === "proses") {
|
|
||||||
router.push(`/investment/${id}/(transaction-flow)/process`);
|
|
||||||
} else if (status === "berhasil") {
|
|
||||||
router.push(`/investment/${id}/(transaction-flow)/success`);
|
|
||||||
} else if (status === "gagal") {
|
|
||||||
router.push(`/investment/${id}/(transaction-flow)/failed`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter>
|
<>
|
||||||
{loadList ? (
|
<Investment_ScreenTransaction />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText/>
|
|
||||||
) : (
|
|
||||||
list.map((item: any, i: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={i}
|
|
||||||
paddingTop={7}
|
|
||||||
paddingBottom={7}
|
|
||||||
onPress={() => {
|
|
||||||
handlePress({
|
|
||||||
id: item.id,
|
|
||||||
status: _.lowerCase(item.statusInvoice),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom truncate>{item?.title || "-"}</TextCustom>
|
|
||||||
<TextCustom color="gray" size="small">
|
|
||||||
{formatChatTime(item?.createdAt)}
|
|
||||||
</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={1}>
|
|
||||||
<View />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={5} style={{ alignItems: "flex-end" }}>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal) || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
variant="light"
|
|
||||||
color={handlerColor(_.lowerCase(item.statusInvoice))}
|
|
||||||
style={GStyles.alignSelfFlexEnd}
|
|
||||||
>
|
|
||||||
{item?.statusInvoice || "-"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
import Investment_ScreenListOfDocument from "@/screens/Invesment/Document/ScreenListDocument";
|
||||||
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
|
|
||||||
import { apiInvestmentGetDocument } from "@/service/api-client/api-investment";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentListOfDocument() {
|
export default function InvestmentListOfDocument() {
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
console.log("ID >> ", id);
|
|
||||||
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadListDocument();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadListDocument = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetDocument({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-document",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<>
|
||||||
{loadList ? (
|
<Investment_ScreenListOfDocument />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<Investment_BoxDetailDocument
|
|
||||||
key={index}
|
|
||||||
title={item.title}
|
|
||||||
href={`/(file)/${item.fileId}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,213 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenRecapOfDocument from "@/screens/Invesment/Document/ScreenRecapOfDocument";
|
||||||
AlertDefaultSystem,
|
|
||||||
BackButton,
|
|
||||||
DotButton,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
|
||||||
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
|
|
||||||
import {
|
|
||||||
apiInvestmentDeleteDocument,
|
|
||||||
apiInvestmentGetDocument,
|
|
||||||
} from "@/service/api-client/api-investment";
|
|
||||||
import { AntDesign, Ionicons } from "@expo/vector-icons";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function InvestmentRecapOfDocument() {
|
export default function InvestmentRecapOfDocument() {
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [openDrawerBox, setOpenDrawerBox] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [selectId, setSelectId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadListDocument();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadListDocument = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetDocument({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-document",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlerDeleteDocument = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiInvestmentDeleteDocument({
|
|
||||||
id: selectId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Data berhasil dihapus",
|
|
||||||
});
|
|
||||||
setList((prev: any[] | null) => {
|
|
||||||
if (!prev) return null;
|
|
||||||
return prev.filter((item: any) => item.id !== selectId);
|
|
||||||
});
|
|
||||||
setOpenDrawerBox(false);
|
|
||||||
setSelectId(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal menghapus data",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Investment_ScreenRecapOfDocument />
|
||||||
options={{
|
|
||||||
title: "Rekap Dokumen",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => (
|
|
||||||
<DotButton
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawer(true);
|
|
||||||
setOpenDrawerBox(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<Investment_BoxDetailDocument
|
|
||||||
key={index}
|
|
||||||
title={item.title}
|
|
||||||
leftIcon={
|
|
||||||
<Ionicons
|
|
||||||
name="ellipsis-horizontal-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color={MainColor.white}
|
|
||||||
style={{
|
|
||||||
zIndex: 10,
|
|
||||||
alignSelf: "flex-end",
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectId(item.id);
|
|
||||||
setOpenDrawerBox(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
href={`/(file)/${item.fileId}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
{/* Drawer On Header */}
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<AntDesign
|
|
||||||
name="plus-circle"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color={MainColor.white}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
label: "Tambah Dokumen",
|
|
||||||
path: `/investment/${id}/(document)/add-document`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
router.push(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
|
|
||||||
{/* Drawer On Box */}
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawerBox}
|
|
||||||
closeDrawer={() => setOpenDrawerBox(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconEdit />,
|
|
||||||
label: "Edit Dokumen",
|
|
||||||
path: `/investment/${selectId}/(document)/edit-document`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<Ionicons
|
|
||||||
name="trash-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color={MainColor.white}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
label: "Hapus Dokumen",
|
|
||||||
path: "" as any,
|
|
||||||
color: MainColor.red,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
if (item.path === ("" as any)) {
|
|
||||||
AlertDefaultSystem({
|
|
||||||
title: "Hapus Dokumen",
|
|
||||||
message: "Apakah anda yakin ingin menghapus dokumen ini?",
|
|
||||||
textLeft: "Batal",
|
|
||||||
textRight: "Hapus",
|
|
||||||
onPressRight: () => {
|
|
||||||
handlerDeleteDocument();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
router.push(item.path as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpenDrawerBox(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenListOfNews from "@/screens/Invesment/News/ScreenListOfNews";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentListOfNews() {
|
export default function InvestmentListOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetNews({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-news",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenListOfNews investmentId={id as string} />
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Daftar Berita",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingBlock={5}
|
|
||||||
href={`/investment/[id]/(news)/${item.id}`}
|
|
||||||
>
|
|
||||||
<TextCustom bold>{item.title}</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/investment/${id}/add-news`,
|
|
||||||
icon: <IconPlus />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
router.push(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenRecapOfNews from "@/screens/Invesment/News/ScreenRecapOfNews";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DotButton,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
|
||||||
import {
|
|
||||||
router,
|
|
||||||
Stack,
|
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
} from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentRecapOfNews() {
|
export default function InvestmentRecapOfNews() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiInvestmentGetNews({
|
|
||||||
id: id as string,
|
|
||||||
category: "all-news",
|
|
||||||
});
|
|
||||||
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenRecapOfNews investmentId={id as string} />
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: "Rekap Berita",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ViewWrapper>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingBlock={5}
|
|
||||||
href={`/investment/[id]/(news)/${item.id}`}
|
|
||||||
>
|
|
||||||
<TextCustom bold>{item.title}</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Tambah Berita",
|
|
||||||
path: `/investment/${id}/add-news`,
|
|
||||||
icon: <IconPlus />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
router.push(item.path as any);
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,230 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Investment_ScreenInvoice from "@/screens/Invesment/ScreenInvoice";
|
||||||
BaseBox,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
ButtonCustom,
|
|
||||||
Grid,
|
|
||||||
InformationBox,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import CopyButton from "@/components/Button/CoyButton";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
|
||||||
import {
|
|
||||||
apiInvestmentGetInvoice,
|
|
||||||
apiInvestmentUpdateInvoice,
|
|
||||||
} from "@/service/api-client/api-investment";
|
|
||||||
import { uploadFileService } from "@/service/upload-service";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function InvestmentInvoice() {
|
export default function InvestmentInvoice() {
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const [data, setData] = useState<any>({});
|
|
||||||
const [image, setImage] = useState<IFileData>({
|
|
||||||
name: "",
|
|
||||||
uri: "",
|
|
||||||
size: 0,
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiInvestmentGetInvoice({
|
|
||||||
id: id as string,
|
|
||||||
category: "invoice",
|
|
||||||
});
|
|
||||||
|
|
||||||
setData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlerSubmitUpdate = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const responseUploadImage = await uploadFileService({
|
|
||||||
dirId: DIRECTORY_ID.investasi_bukti_transfer,
|
|
||||||
imageUri: image?.uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!responseUploadImage?.data?.id) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal mengunggah bukti transfer",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await apiInvestmentUpdateInvoice({
|
|
||||||
id: id as string,
|
|
||||||
data: {
|
|
||||||
imageId: responseUploadImage?.data?.id,
|
|
||||||
},
|
|
||||||
status: "proses",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Berhasil mengunggah bukti transfer",
|
|
||||||
});
|
|
||||||
router.push(`/investment/${id}/(transaction-flow)/process`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper>
|
<Investment_ScreenInvoice />
|
||||||
<StackCustom>
|
|
||||||
<InformationBox text="Mohon transfer ke rekening dibawah" />
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={4}>
|
|
||||||
<TextCustom>Bank</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<Spacing height={10} />
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={4}>
|
|
||||||
<TextCustom>Nama Akun</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
|
||||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
|
||||||
<Grid.Col
|
|
||||||
span={8}
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextCustom size="xlarge" bold color="yellow">
|
|
||||||
{data?.MasterBank?.norek}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={4}
|
|
||||||
style={{
|
|
||||||
alignItems: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CopyButton textToCopy={data?.MasterBank?.norek} />
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom>Jumlah Transaksi</TextCustom>
|
|
||||||
|
|
||||||
<Spacing height={10} />
|
|
||||||
|
|
||||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
|
||||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
|
||||||
<Grid.Col
|
|
||||||
span={8}
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextCustom size="xlarge" bold color="yellow">
|
|
||||||
Rp. {formatCurrencyDisplay(data?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={4}
|
|
||||||
style={{
|
|
||||||
alignItems: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CopyButton textToCopy={data?.nominal} />
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom align="center">
|
|
||||||
Upload bukti transfer anda.
|
|
||||||
</TextCustom>
|
|
||||||
{image ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 10,
|
|
||||||
paddingInline: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextCustom bold align="center" truncate>
|
|
||||||
{image?.name}
|
|
||||||
</TextCustom>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<TextCustom align="center">
|
|
||||||
Tidak ada gambar yang diunggah
|
|
||||||
</TextCustom>
|
|
||||||
)}
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
onPress={() => {
|
|
||||||
pickFile({
|
|
||||||
allowedType: "image",
|
|
||||||
setImageUri(file: any) {
|
|
||||||
setImage(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon="upload"
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={!image || isLoading}
|
|
||||||
onPress={() => {
|
|
||||||
handlerSubmitUpdate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Saya Sudah Transfer
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
|
||||||
<Spacing />
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export default function InvestmentSelectBank() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log("[RESPONSE >>]", response);
|
|
||||||
const invoiceId = response.data.id;
|
const invoiceId = response.data.id;
|
||||||
|
|
||||||
const delStorage = await AsyncStorage.removeItem(
|
const delStorage = await AsyncStorage.removeItem(
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export default function InvestmentDetailStatus() {
|
|||||||
updateCountDown();
|
updateCountDown();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
const updateCountDown = () => {
|
const updateCountDown = () => {
|
||||||
const countDown = countDownAndCondition({
|
const countDown = countDownAndCondition({
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextInputCustom,
|
TextInputCustom
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||||
<LandscapeFrameUploaded
|
<LandscapeFrameUploaded
|
||||||
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
disabled
|
iconLeft="Rp."
|
||||||
|
// disabled
|
||||||
required
|
required
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
label="Total Lembar"
|
label="Total Lembar"
|
||||||
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing height={50} />
|
</NewWrapper>
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export default function InvestmentDetail() {
|
|||||||
updateCountDown();
|
updateCountDown();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
const updateCountDown = () => {
|
const updateCountDown = () => {
|
||||||
const countDown = countDownAndCondition({
|
const countDown = countDownAndCondition({
|
||||||
|
|||||||
@@ -1,67 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import Investment_ScreenInvestor from "@/screens/Invesment/ScreenInvestor";
|
||||||
import {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
AvatarUsernameAndOtherComponent,
|
|
||||||
BoxWithHeaderSection,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { apiInvestmentGetInvestorById } from "@/service/api-client/api-investment";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function InvestmentInvestor() {
|
export default function InvestmentInvestor() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingList(true);
|
|
||||||
const response = await apiInvestmentGetInvestorById({
|
|
||||||
id: id as string,
|
|
||||||
})
|
|
||||||
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingList(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Investment_ScreenInvestor investmentId={id as string} />
|
||||||
<ViewWrapper>
|
|
||||||
{loadingList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<BoxWithHeaderSection key={index}>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.Author?.Profile?.imageId}
|
|
||||||
name={item?.Author?.username}
|
|
||||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
|
||||||
/>
|
|
||||||
<TextCustom bold>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
|
||||||
</TextCustom>
|
|
||||||
</BoxWithHeaderSection>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
BaseBox,
|
BaseBox,
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
CenterCustom,
|
CenterCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
LoaderCustom,
|
NewWrapper,
|
||||||
SelectCustom,
|
SelectCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
|
|||||||
|
|
||||||
// const [coba, setCoba] = useState("");
|
// const [coba, setCoba] = useState("");
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
footerComponent={
|
||||||
|
<BoxButtonOnFooter>
|
||||||
|
<ButtonCustom
|
||||||
|
disabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onPress={() => handleSubmit()}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||||
<LandscapeFrameUploaded image={image as string} />
|
<LandscapeFrameUploaded image={image as string} />
|
||||||
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
|
|||||||
|
|
||||||
<StackCustom gap={0}>
|
<StackCustom gap={0}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
disabled
|
iconLeft="Rp."
|
||||||
|
// disabled
|
||||||
|
editable={false}
|
||||||
required
|
required
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
label="Total Lembar"
|
label="Total Lembar"
|
||||||
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{loadingMaster ? (
|
{loadingMaster ? (
|
||||||
<LoaderCustom />
|
<CustomSkeleton height={50} />
|
||||||
) : (
|
) : (
|
||||||
<SelectCustom
|
<SelectCustom
|
||||||
required
|
required
|
||||||
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loadingMaster ? (
|
{loadingMaster ? (
|
||||||
<LoaderCustom />
|
<CustomSkeleton height={50} />
|
||||||
) : (
|
) : (
|
||||||
<SelectCustom
|
<SelectCustom
|
||||||
required
|
required
|
||||||
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loadingMaster ? (
|
{loadingMaster ? (
|
||||||
<LoaderCustom />
|
<CustomSkeleton height={50} />
|
||||||
) : (
|
) : (
|
||||||
<SelectCustom
|
<SelectCustom
|
||||||
required
|
required
|
||||||
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<ButtonCustom
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onPress={() => handleSubmit()}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
<Spacing height={50} />
|
{/* <Spacing height={50} /> */}
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function JobArchive() {
|
export default function JobArchive() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
|
||||||
const [isLoadData, setIsLoadData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoadData(true);
|
|
||||||
const response = await apiJobGetAll({
|
|
||||||
category: "archive",
|
|
||||||
authorId: user?.id,
|
|
||||||
});
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper hideFooter>
|
<>
|
||||||
{isLoadData ? (
|
<Job_ScreenArchive2 />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item, index) => (
|
|
||||||
<BaseBox
|
|
||||||
key={index}
|
|
||||||
paddingTop={20}
|
|
||||||
paddingBottom={20}
|
|
||||||
href={`/job/${item.id}/archive`}
|
|
||||||
>
|
|
||||||
<TextCustom align="center" bold truncate size="large">
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,10 @@
|
|||||||
import {
|
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
|
||||||
AvatarUsernameAndOtherComponent,
|
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
|
||||||
BoxWithHeaderSection,
|
|
||||||
FloatingButton,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function JobBeranda() {
|
export default function JobBeranda() {
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
|
||||||
const [isLoadData, setIsLoadData] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData(search);
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async (search: string) => {
|
|
||||||
try {
|
|
||||||
setIsLoadData(true);
|
|
||||||
const response = await apiJobGetAll({ search, category: "beranda" });
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = (search: string) => {
|
|
||||||
setSearch(search);
|
|
||||||
onLoadData(search);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Job_ScreenBeranda2 />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/job/create")} />
|
|
||||||
}
|
|
||||||
headerComponent={
|
|
||||||
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Belum ada lowongan</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item, index) => (
|
|
||||||
<BoxWithHeaderSection
|
|
||||||
key={index}
|
|
||||||
onPress={() => router.push(`/job/${item.id}`)}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.Author?.Profile?.imageId}
|
|
||||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
|
||||||
name={item?.Author?.username}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextCustom truncate={2} align="center" bold size="large">
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
<Spacing />
|
|
||||||
</BoxWithHeaderSection>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<Spacing />
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,12 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
|
||||||
BaseBox,
|
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import { apiJobGetByStatus } from "@/service/api-client/api-job";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function JobStatus() {
|
export default function JobStatus() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
|
||||||
console.log("STATUS", status);
|
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish"
|
|
||||||
);
|
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
|
||||||
const [isLoadList, setIsLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [user?.id, activeCategory])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoadList(true);
|
|
||||||
const response = await apiJobGetByStatus({
|
|
||||||
authorId: user?.id as string,
|
|
||||||
status: activeCategory as string,
|
|
||||||
});
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
{/* <Job_MainViewStatus /> */}
|
||||||
{isLoadList ? (
|
<Job_MainViewStatus2 />
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">
|
|
||||||
Tidak ada data {activeCategory}
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((e, i) => (
|
|
||||||
<BaseBox
|
|
||||||
key={i}
|
|
||||||
paddingTop={20}
|
|
||||||
paddingBottom={20}
|
|
||||||
href={`/job/${e?.id}/${activeCategory}/detail`}
|
|
||||||
>
|
|
||||||
<TextCustom align="center" bold truncate size="large">
|
|
||||||
{e?.title}
|
|
||||||
</TextCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
|
BoxButtonOnFooter,
|
||||||
ButtonCenteredOnly,
|
ButtonCenteredOnly,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
InformationBox,
|
InformationBox,
|
||||||
LandscapeFrameUploaded,
|
LandscapeFrameUploaded,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextInputCustom,
|
TextInputCustom
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
import DIRECTORY_ID from "@/constants/directory-id";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -99,16 +100,17 @@ export default function JobCreate() {
|
|||||||
const buttonSubmit = () => {
|
const buttonSubmit = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
<BoxButtonOnFooter>
|
||||||
Simpan
|
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||||
</ButtonCustom>
|
Simpan
|
||||||
<Spacing />
|
</ButtonCustom>
|
||||||
|
</BoxButtonOnFooter>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper>
|
<NewWrapper footerComponent={buttonSubmit()}>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
|
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
|
||||||
|
|
||||||
@@ -160,9 +162,7 @@ export default function JobCreate() {
|
|||||||
value={data.deskripsi}
|
value={data.deskripsi}
|
||||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{buttonSubmit()}
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import ScreenNotification from "@/screens/Notification/ScreenNotification";
|
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
|
||||||
|
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
|
||||||
|
|
||||||
export default function Notification() {
|
export default function Notification() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScreenNotification />
|
<ScreenNotification_V2 />
|
||||||
|
{/* <ScreenNotification_V1 /> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export default function ProfileDetailBlocked() {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const response = await apiGetBlockedById({ id: String(id) });
|
const response = await apiGetBlockedById({ id: String(id) });
|
||||||
// console.log("[RESPONSE >>]", JSON.stringify(response, null, 2));
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
|
||||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
|
|
||||||
export default function VotingContribution() {
|
export default function VotingContribution() {
|
||||||
const { user } = useAuth();
|
return <Voting_ScreenContribution />;
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingGetData(true);
|
|
||||||
const response = await apiVotingGetAll({
|
|
||||||
category: "contribution",
|
|
||||||
authorId: user?.id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingGetData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewWrapper hideFooter>
|
|
||||||
{loadingGetData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
|
|
||||||
) : listData.map((item: any, index: number) => (
|
|
||||||
<Voting_BoxPublishSection
|
|
||||||
data={item}
|
|
||||||
key={index}
|
|
||||||
href={`/voting/${item.id}/contribution`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
|
||||||
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
|
|
||||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
export default function VotingHistory() {
|
export default function VotingHistory() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
|
||||||
|
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [activeCategory])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingGetData(true);
|
|
||||||
const response = await apiVotingGetAll({
|
|
||||||
category: activeCategory === "all" ? "all-history" : "my-history",
|
|
||||||
authorId: user?.id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingGetData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Voting_ScreenHistory />
|
||||||
headerComponent={
|
</>
|
||||||
<TabsTwoButtonCustom
|
|
||||||
leftValue="all"
|
|
||||||
rightValue="main"
|
|
||||||
leftText="Semua Riwayat"
|
|
||||||
rightText="Riwayat Saya"
|
|
||||||
activeCategory={activeCategory}
|
|
||||||
handlePress={handlePress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingGetData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada riwayat</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<Voting_BoxPublishSection
|
|
||||||
key={index}
|
|
||||||
id={item.id}
|
|
||||||
data={item}
|
|
||||||
href={`/voting/${item.id}/history`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
|
||||||
FloatingButton,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
|
||||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function VotingBeranda() {
|
export default function VotingBeranda() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingGetData(true);
|
|
||||||
const response = await apiVotingGetAll({
|
|
||||||
search,
|
|
||||||
category: "beranda",
|
|
||||||
userLoginId: user?.id,
|
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingGetData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<>
|
||||||
hideFooter
|
<Voting_ScreenBeranda />
|
||||||
floatingButton={
|
</>
|
||||||
<FloatingButton onPress={() => router.push("/voting/create")} />
|
|
||||||
}
|
|
||||||
headerComponent={
|
|
||||||
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingGetData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<Voting_BoxPublishSection
|
|
||||||
data={item}
|
|
||||||
key={index}
|
|
||||||
href={`/voting/${item.id}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
LoaderCustom,
|
|
||||||
ScrollableCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
|
||||||
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function VotingStatus() {
|
export default function VotingStatus() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
|
||||||
|
|
||||||
const id = user?.id || "";
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
||||||
status || "publish"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [listData, setListData] = useState([]);
|
|
||||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [activeCategory, id])
|
|
||||||
);
|
|
||||||
|
|
||||||
async function onLoadData() {
|
|
||||||
try {
|
|
||||||
setLoadingGetData(true);
|
|
||||||
const response = await apiVotingGetByStatus({
|
|
||||||
id: id as string,
|
|
||||||
status: activeCategory!,
|
|
||||||
});
|
|
||||||
setListData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
setLoadingGetData(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<ScrollableCustom
|
|
||||||
data={dummyMasterStatus.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
<>
|
||||||
{loadingGetData ? (
|
<Voting_ScreenStatus />
|
||||||
<LoaderCustom />
|
</>
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, i: number) => (
|
|
||||||
<BaseBox
|
|
||||||
key={i}
|
|
||||||
paddingTop={20}
|
|
||||||
paddingBottom={20}
|
|
||||||
href={`/voting/${item.id}/${activeCategory}/detail`}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom align="center" bold truncate={2} size="large">
|
|
||||||
{item?.title || ""}
|
|
||||||
</TextCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ width: "70%", alignSelf: "center" }}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
{item?.awalVote &&
|
|
||||||
dateTimeView({
|
|
||||||
date: item?.awalVote,
|
|
||||||
withoutTime: true,
|
|
||||||
})}{" "}
|
|
||||||
-{" "}
|
|
||||||
{item?.akhirVote &&
|
|
||||||
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
|
|
||||||
</BadgeCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
|
|||||||
setLoadingGetData(true);
|
setLoadingGetData(true);
|
||||||
const response = await apiVotingGetOne({ id: id as string });
|
const response = await apiVotingGetOne({ id: id as string });
|
||||||
|
|
||||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import {
|
|||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
CenterCustom,
|
CenterCustom,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
TextInputCustom,
|
TextInputCustom
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||||
@@ -34,7 +35,7 @@ interface IEditData {
|
|||||||
Voting_DaftarNamaVote?: [
|
Voting_DaftarNamaVote?: [
|
||||||
{
|
{
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export default function VotingEdit() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -188,9 +189,9 @@ export default function VotingEdit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
<NewWrapper footerComponent={buttonSubmit()}>
|
||||||
{loadingGetData ? (
|
{loadingGetData ? (
|
||||||
<LoaderCustom />
|
<ListSkeletonComponent />
|
||||||
) : (
|
) : (
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
@@ -210,7 +211,7 @@ export default function VotingEdit() {
|
|||||||
onChangeText={(text) => setData({ ...data, deskripsi: text })}
|
onChangeText={(text) => setData({ ...data, deskripsi: text })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<DateTimePickerCustom
|
<DateTimePickerCustom
|
||||||
minimumDate={new Date(Date.now())}
|
minimumDate={new Date(Date.now())}
|
||||||
@@ -255,7 +256,7 @@ export default function VotingEdit() {
|
|||||||
}
|
}
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|
||||||
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
|
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
|
||||||
@@ -270,7 +271,7 @@ export default function VotingEdit() {
|
|||||||
...(data as any),
|
...(data as any),
|
||||||
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
|
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
|
||||||
(item: any, i: any) =>
|
(item: any, i: any) =>
|
||||||
i === index ? { ...item, value } : item
|
i === index ? { ...item, value } : item,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -327,6 +328,6 @@ export default function VotingEdit() {
|
|||||||
<Spacing />
|
<Spacing />
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
)}
|
)}
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
|
||||||
AvatarUsernameAndOtherComponent,
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
LoaderCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { apiVotingContribution } from "@/service/api-client/api-voting";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function Voting_ListOfContributor() {
|
export default function VotingListOfContributor() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Voting_ScreenListOfContributor />;
|
||||||
const [listData, setListData] = useState<any>([]);
|
|
||||||
const [isLoadData, setIsLoadData] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadList();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoadData(true);
|
|
||||||
const response = await apiVotingContribution({
|
|
||||||
id: id as string,
|
|
||||||
authorId: "",
|
|
||||||
category: "list",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewWrapper>
|
|
||||||
{isLoadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center">Tidak ada kontributor</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData.map((item: any, index: number) => (
|
|
||||||
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
|
|
||||||
<AvatarUsernameAndOtherComponent
|
|
||||||
avatar={item?.Author?.Profile?.imageId || ""}
|
|
||||||
name={item?.Author?.username || "Username"}
|
|
||||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
|
||||||
rightComponent={
|
|
||||||
<BadgeCustom style={{ alignSelf: "flex-end" }}>
|
|
||||||
{item?.Voting_DaftarNamaVote?.value}
|
|
||||||
</BadgeCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import {
|
|||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
CenterCustom,
|
CenterCustom,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
TextInputCustom,
|
TextInputCustom,
|
||||||
ViewWrapper
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -79,7 +80,7 @@ export default function VotingCreate() {
|
|||||||
type: "success",
|
type: "success",
|
||||||
text1: "Data berhasil disimpan",
|
text1: "Data berhasil disimpan",
|
||||||
});
|
});
|
||||||
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
|
router.replace("/voting/(tabs)/status?status=review");
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -106,7 +107,7 @@ export default function VotingCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
<NewWrapper footerComponent={buttonSubmit()}>
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
label="Judul Voting"
|
label="Judul Voting"
|
||||||
@@ -142,7 +143,6 @@ export default function VotingCreate() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{listVote.map((item, index) => (
|
{listVote.map((item, index) => (
|
||||||
<TextInputCustom
|
<TextInputCustom
|
||||||
key={index}
|
key={index}
|
||||||
@@ -153,8 +153,8 @@ export default function VotingCreate() {
|
|||||||
onChangeText={(value: any) =>
|
onChangeText={(value: any) =>
|
||||||
setListVote(
|
setListVote(
|
||||||
listVote.map((item, i) =>
|
listVote.map((item, i) =>
|
||||||
i === index ? { ...item, value } : item
|
i === index ? { ...item, value } : item,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -198,6 +198,6 @@ export default function VotingCreate() {
|
|||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
||||||
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
||||||
|
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
|
||||||
|
import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
import {
|
import {
|
||||||
ICON_SIZE_MEDIUM,
|
ICON_SIZE_MEDIUM,
|
||||||
@@ -20,6 +22,10 @@ import {
|
|||||||
adminListMenu,
|
adminListMenu,
|
||||||
superAdminListMenu,
|
superAdminListMenu,
|
||||||
} from "@/screens/Admin/listPageAdmin";
|
} from "@/screens/Admin/listPageAdmin";
|
||||||
|
import {
|
||||||
|
adminListMenu_V2,
|
||||||
|
superAdminListMenu_V2,
|
||||||
|
} from "@/screens/Admin/listPageAdmin_V2";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import { GStyles } from "@/styles/global-styles";
|
||||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
@@ -148,6 +154,24 @@ export default function AdminLayout() {
|
|||||||
}
|
}
|
||||||
onClose={() => setOpenDrawerNavbar(false)}
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V2
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V3
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</DrawerAdmin>
|
</DrawerAdmin>
|
||||||
|
|
||||||
@@ -198,7 +222,7 @@ export default function AdminLayout() {
|
|||||||
// size={ICON_SIZE_SMALL}
|
// size={ICON_SIZE_SMALL}
|
||||||
// color={MainColor.white}
|
// color={MainColor.white}
|
||||||
// />
|
// />
|
||||||
<AdminNotificationBell/>
|
<AdminNotificationBell />
|
||||||
),
|
),
|
||||||
path: "/admin/notification",
|
path: "/admin/notification",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,213 +1,10 @@
|
|||||||
import {
|
import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
|
||||||
AlertDefaultSystem,
|
import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
|
||||||
BackButton,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
NewWrapper,
|
|
||||||
ScrollableCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
|
||||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
|
||||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
|
||||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
|
||||||
import { formatChatTime } from "@/utils/formatChatTime";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { router, Stack, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { RefreshControl, View } from "react-native";
|
|
||||||
|
|
||||||
const selectedCategory = (value: string) => {
|
|
||||||
const category = listOfcategoriesAppNotification.find(
|
|
||||||
(c) => c.value === value
|
|
||||||
);
|
|
||||||
return category?.label;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BoxNotification = ({
|
|
||||||
data,
|
|
||||||
activeCategory,
|
|
||||||
}: {
|
|
||||||
data: any;
|
|
||||||
activeCategory: string | null;
|
|
||||||
}) => {
|
|
||||||
const { markAsRead } = useNotificationStore();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BaseBox
|
|
||||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
|
||||||
onPress={() => {
|
|
||||||
console.log(
|
|
||||||
"Notification >",
|
|
||||||
selectedCategory(activeCategory as string)
|
|
||||||
);
|
|
||||||
router.push(data.deepLink);
|
|
||||||
markAsRead(data.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<TextCustom truncate={2} bold>
|
|
||||||
{data.title}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
|
||||||
|
|
||||||
<TextCustom size="small" color="gray">
|
|
||||||
{formatChatTime(data.createdAt)}
|
|
||||||
</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminNotification() {
|
export default function AdminNotification() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
|
|
||||||
const { markAsReadAll } = useNotificationStore();
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
fecthData();
|
|
||||||
}, [activeCategory])
|
|
||||||
);
|
|
||||||
|
|
||||||
const fecthData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await apiGetNotificationsById({
|
|
||||||
id: user?.id as any,
|
|
||||||
category: activeCategory as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
} else {
|
|
||||||
setListData([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error Notification", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRefresh = () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
fecthData();
|
|
||||||
setRefreshing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Admin_ScreenNotification2 />
|
||||||
options={{
|
|
||||||
title: "Admin Notifikasi",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => (
|
|
||||||
<IconDot
|
|
||||||
color={MainColor.yellow}
|
|
||||||
onPress={() => setOpenDrawer(true)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<ScrollableCustom
|
|
||||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
|
||||||
id: i,
|
|
||||||
label: e.label,
|
|
||||||
value: e.value,
|
|
||||||
}))}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as string}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<ListSkeletonComponent />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<NoDataText text="Belum ada notifikasi" />
|
|
||||||
) : (
|
|
||||||
listData.map((e, i) => (
|
|
||||||
<View key={i}>
|
|
||||||
<BoxNotification
|
|
||||||
data={e}
|
|
||||||
activeCategory={activeCategory as any}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</NewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Tandai Semua Dibaca",
|
|
||||||
value: "read-all",
|
|
||||||
icon: (
|
|
||||||
<Ionicons
|
|
||||||
name="reader-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color={MainColor.white}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
path: "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item: any) => {
|
|
||||||
console.log("Item", item.value);
|
|
||||||
if (item.value === "read-all") {
|
|
||||||
AlertDefaultSystem({
|
|
||||||
title: "Tandai Semua Dibaca",
|
|
||||||
message:
|
|
||||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
|
||||||
textLeft: "Batal",
|
|
||||||
textRight: "Ya",
|
|
||||||
onPressRight: () => {
|
|
||||||
markAsReadAll(user?.id as any);
|
|
||||||
const data = _.cloneDeep(listData);
|
|
||||||
data.forEach((e) => {
|
|
||||||
e.isRead = true;
|
|
||||||
});
|
|
||||||
setListData(data);
|
|
||||||
onRefresh();
|
|
||||||
setOpenDrawer(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||||
import { Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
|
// Setelah (dengan penanganan):
|
||||||
|
const handleBack = () => {
|
||||||
|
if (router.canGoBack()) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
// Alternatif action ketika tidak bisa kembali
|
||||||
|
router.replace('/'); // atau navigasi ke halaman default
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton /> }}
|
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
<StackCustom
|
<StackCustom
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
// DateTimeInput.tsx
|
// DateTimeInput.tsx
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import { GStyles } from "@/styles/global-styles";
|
||||||
@@ -7,7 +6,14 @@ import DateTimePicker, {
|
|||||||
DateTimePickerEvent,
|
DateTimePickerEvent,
|
||||||
} from "@react-native-community/datetimepicker";
|
} from "@react-native-community/datetimepicker";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { Pressable, StyleProp, Text, View, ViewStyle } from "react-native";
|
import {
|
||||||
|
Keyboard,
|
||||||
|
Pressable,
|
||||||
|
StyleProp,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from "react-native";
|
||||||
import Grid from "../Grid/GridCustom";
|
import Grid from "../Grid/GridCustom";
|
||||||
import TextCustom from "../Text/TextCustom";
|
import TextCustom from "../Text/TextCustom";
|
||||||
|
|
||||||
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
|||||||
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
|
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
|
||||||
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
|
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
|
||||||
|
|
||||||
console.log("Date Android", value)
|
console.log("Date Android", value);
|
||||||
|
|
||||||
// Fungsi untuk menggabungkan tanggal dan waktu
|
// Fungsi untuk menggabungkan tanggal dan waktu
|
||||||
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
|
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
|
||||||
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
|||||||
time.getHours(),
|
time.getHours(),
|
||||||
time.getMinutes(),
|
time.getMinutes(),
|
||||||
time.getSeconds(),
|
time.getSeconds(),
|
||||||
time.getMilliseconds()
|
time.getMilliseconds(),
|
||||||
);
|
);
|
||||||
return combined;
|
return combined;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleDatePicker = () => {
|
const toggleDatePicker = () => {
|
||||||
|
Keyboard.dismiss();
|
||||||
setShowDate(!showDate);
|
setShowDate(!showDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTimePicker = () => {
|
const toggleTimePicker = () => {
|
||||||
|
Keyboard.dismiss();
|
||||||
setShowTime(!showTime);
|
setShowTime(!showTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
// DateTimeInput.tsx
|
// DateTimeInput.tsx
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import { GStyles } from "@/styles/global-styles";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import DateTimePicker, {
|
import DateTimePicker, {
|
||||||
DateTimePickerEvent,
|
DateTimePickerEvent,
|
||||||
} from "@react-native-community/datetimepicker";
|
} from "@react-native-community/datetimepicker";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import { Button, StyleProp, Text, View, ViewStyle } from "react-native";
|
import {
|
||||||
|
Button,
|
||||||
|
StyleProp,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
Keyboard,
|
||||||
|
TouchableOpacity,
|
||||||
|
Modal,
|
||||||
|
} from "react-native";
|
||||||
import ClickableCustom from "../Clickable/ClickableCustom";
|
import ClickableCustom from "../Clickable/ClickableCustom";
|
||||||
import TextCustom from "../Text/TextCustom";
|
import TextCustom from "../Text/TextCustom";
|
||||||
|
|
||||||
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
||||||
value as any
|
value as any,
|
||||||
|
);
|
||||||
|
// State sementara untuk menyimpan nilai yang dipilih
|
||||||
|
const [tempSelectedDate, setTempSelectedDate] = useState<Date | undefined>(
|
||||||
|
value as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleConfirm = (event: any, date?: Date) => {
|
const handleConfirm = (event: any, date?: Date) => {
|
||||||
if (event.type === "set" && date !== undefined) {
|
if (event.type === "set" && date !== undefined) {
|
||||||
setSelectedDate(date);
|
// Hanya perbarui state sementara, bukan state utama
|
||||||
onChange(date as any);
|
setTempSelectedDate(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
|
// Sembunyikan keyboard sebelum menampilkan date picker
|
||||||
|
Keyboard.dismiss();
|
||||||
|
// Set state sementara ke nilai saat ini
|
||||||
|
setTempSelectedDate(selectedDate);
|
||||||
setShow(!show);
|
setShow(!show);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fungsi untuk menangani klik di luar area picker
|
||||||
|
const handleOutsidePress = () => {
|
||||||
|
if (show) {
|
||||||
|
setShow(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClickableCustom
|
<ClickableCustom
|
||||||
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
|||||||
))}
|
))}
|
||||||
</ClickableCustom>
|
</ClickableCustom>
|
||||||
|
|
||||||
{show && (
|
<Modal
|
||||||
<>
|
animationType="fade"
|
||||||
<View
|
transparent={true}
|
||||||
|
visible={show}
|
||||||
|
onRequestClose={() => setShow(false)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)", // Efek blur dengan background semi-transparan
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: 15,
|
top: 0,
|
||||||
backgroundColor: "white",
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 10,
|
|
||||||
// top: 0,
|
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
borderColor: "#ccc",
|
}}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={handleOutsidePress}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1 }} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
zIndex: 15,
|
||||||
|
backgroundColor: MainColor.white_gray,
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
width: "95%",
|
||||||
|
maxWidth: 400,
|
||||||
|
borderColor: MainColor.placeholder,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
}}
|
}}
|
||||||
|
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
|
||||||
|
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
|
||||||
>
|
>
|
||||||
{/* <View style={{ alignItems: "flex-start" }}>
|
{/* <View
|
||||||
<Ionicons
|
style={{
|
||||||
name="close"
|
flexDirection: "row",
|
||||||
size={20}
|
justifyContent: "space-between",
|
||||||
color="black"
|
alignItems: "center",
|
||||||
onPress={() => {
|
marginBottom: 10,
|
||||||
setShow(false);
|
}}
|
||||||
setSelectedDate(undefined);
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: MainColor.black,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
Pilih Tanggal & Waktu
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShow(false)}>
|
||||||
|
<Ionicons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={AccentColor.blackgray}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
</View> */}
|
</View> */}
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={selectedDate || new Date()}
|
value={tempSelectedDate || new Date()}
|
||||||
mode={"datetime"}
|
mode={"datetime"}
|
||||||
display="spinner"
|
display="inline"
|
||||||
onChange={handleConfirm}
|
onChange={handleConfirm}
|
||||||
minimumDate={minimumDate}
|
minimumDate={minimumDate}
|
||||||
maximumDate={maximumDate}
|
maximumDate={maximumDate}
|
||||||
themeVariant="light"
|
themeVariant="light"
|
||||||
/>
|
/>
|
||||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
<View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
|
||||||
<ClickableCustom
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShow(false)
|
setShow(false);
|
||||||
setSelectedDate(undefined)
|
// Kembalikan ke nilai sebelumnya jika batal
|
||||||
|
setTempSelectedDate(selectedDate);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
flex: 1,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: MainColor.placeholder,
|
backgroundColor: MainColor.placeholder,
|
||||||
marginTop: 10,
|
|
||||||
width: "48%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextCustom color="black">Batal</TextCustom>
|
<TextCustom color="black">Batal</TextCustom>
|
||||||
</ClickableCustom>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<ClickableCustom
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShow(false)
|
// Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
|
||||||
onChange(selectedDate as any)
|
setSelectedDate(tempSelectedDate);
|
||||||
|
onChange(tempSelectedDate as any);
|
||||||
|
setShow(false);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
flex: 1,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: MainColor.darkblue,
|
backgroundColor: MainColor.darkblue,
|
||||||
marginTop: 10,
|
|
||||||
width: "48%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextCustom>OK</TextCustom>
|
<TextCustom>OK</TextCustom>
|
||||||
</ClickableCustom>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</View>
|
||||||
)}
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
276
components/Drawer/NavbarMenu.back.tsx
Normal file
276
components/Drawer/NavbarMenu.back.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Set activeLink saat pathname berubah
|
||||||
|
useEffect(() => {
|
||||||
|
if (normalizedPathname) {
|
||||||
|
setActiveLink(normalizedPathname);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
// flex: 1,
|
||||||
|
// backgroundColor: MainColor.black,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10, // Opsional: tambahkan padding
|
||||||
|
}}
|
||||||
|
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
activeLink={activeLink}
|
||||||
|
setActiveLink={setActiveLink}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
activeLink,
|
||||||
|
setActiveLink,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem;
|
||||||
|
onClose?: () => void;
|
||||||
|
activeLink: string | null;
|
||||||
|
setActiveLink: (link: string | null) => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const isActive = activeLink === item.link;
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
React.useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.parentText}>{item.label}</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
// {
|
||||||
|
// backgroundColor: "red",
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 40],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.links.map((subItem, index) => {
|
||||||
|
const isSubActive = activeLink === subItem.link;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, isSubActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(subItem.link);
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={isSubActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
isSubActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(item.link || null);
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -28,7 +28,7 @@ interface NavbarMenuProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [activeLink, setActiveLink] = useState<string | null>(null);
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (normalizedPathname) {
|
if (normalizedPathname) {
|
||||||
setActiveLink(normalizedPathname);
|
setActiveLink(normalizedPathname);
|
||||||
|
|
||||||
|
// Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
|
||||||
|
for (const item of items) {
|
||||||
|
// Cocokkan dengan link langsung
|
||||||
|
if (item.link && normalizedPathname.startsWith(item.link)) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cocokkan dengan submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
|
||||||
|
if (matchingSubItem) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [normalizedPathname]);
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
// Toggle dropdown
|
// Toggle dropdown
|
||||||
const toggleOpen = (label: string) => {
|
const toggleOpen = (label: string) => {
|
||||||
setOpenKeys((prev) =>
|
setOpenKeys((prev) =>
|
||||||
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,35 +125,71 @@ function MenuItem({
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggleOpen: () => void;
|
toggleOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
// Cek apakah menu ini atau submenu-nya yang aktif
|
||||||
const isActive = activeLink === item.link;
|
const isActive = activeLink === item.link;
|
||||||
|
|
||||||
|
// Cek apakah path saat ini cocok dengan salah satu submenu
|
||||||
|
const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
|
||||||
|
|
||||||
|
// Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
|
||||||
|
const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return activeLink.startsWith(linkPath + "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
|
||||||
|
const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
|
||||||
|
|
||||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
// Animasi saat isOpen berubah
|
// Animasi saat isOpen berubah
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
|
||||||
|
const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
|
||||||
|
|
||||||
Animated.timing(animatedHeight, {
|
Animated.timing(animatedHeight, {
|
||||||
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
duration: 200,
|
duration: 200,
|
||||||
useNativeDriver: false,
|
useNativeDriver: false,
|
||||||
}).start();
|
}).start();
|
||||||
}, [isOpen, item.links, animatedHeight]);
|
|
||||||
|
// Jika perlu membuka dropdown otomatis, panggil toggleOpen
|
||||||
|
if (shouldAutoOpen) {
|
||||||
|
toggleOpen();
|
||||||
|
}
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
|
||||||
|
|
||||||
// Jika ada submenu
|
// Jika ada submenu
|
||||||
if (item.links && item.links.length > 0) {
|
if (item.links && item.links.length > 0) {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{/* Parent Item */}
|
{/* Parent Item */}
|
||||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isMenuActive && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.parentText}>{item.label}</Text>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isMenuActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
parentText: {
|
parentText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V2 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V2[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// Check jika ada submenu yang match dengan current path
|
||||||
|
const hasActiveSubmenu = item.links.some((subItem) => {
|
||||||
|
return checkPathMatch(subItem.link, subItem.detailPattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasActiveSubmenu) {
|
||||||
|
newOpenKeys.push(item.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V2;
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
console.log('🔍 Pattern Match Check:', {
|
||||||
|
currentPath,
|
||||||
|
detailPattern,
|
||||||
|
regex: patternRegex.toString(),
|
||||||
|
isMatch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
const hasActiveSubmenu =
|
||||||
|
item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// PRE-CALCULATE semua active states untuk submenu
|
||||||
|
const submenuActiveStates = item.links.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
hasActiveSubmenu && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
hasActiveSubmenu && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
dashboardLink: subItem.link,
|
||||||
|
dashboardLength: pathLength,
|
||||||
|
otherLabel: other.subItem.label,
|
||||||
|
otherLink: other.subItem.link,
|
||||||
|
otherPattern: other.subItem.detailPattern,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
otherIsActive: other.isActive,
|
||||||
|
isOtherLonger,
|
||||||
|
willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
if (isSubActive && other.isActive) {
|
||||||
|
console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
current: subItem.label,
|
||||||
|
currentPath: subItem.link,
|
||||||
|
currentLength: pathLength,
|
||||||
|
other: other.subItem.label,
|
||||||
|
otherPath: other.subItem.link,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
isOtherLonger,
|
||||||
|
shouldDisableCurrent: isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
if (isSubActive) {
|
||||||
|
console.log('✅ Active check:', {
|
||||||
|
label: subItem.label,
|
||||||
|
link: subItem.link,
|
||||||
|
isSubActive,
|
||||||
|
hasMoreSpecificMatch,
|
||||||
|
finalIsActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, finalIsActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={finalIsActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
finalIsActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V3 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate all potential matches for conflict resolution
|
||||||
|
const allMatches = items.flatMap(item => {
|
||||||
|
if (!item.links || item.links.length === 0) return [];
|
||||||
|
|
||||||
|
return item.links
|
||||||
|
.filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
|
||||||
|
.map(subItem => ({
|
||||||
|
parentLabel: item.label,
|
||||||
|
subItem,
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the most specific match for each parent
|
||||||
|
const uniqueParents = new Map<string, { parentLabel: string, longestPathLength: number }>();
|
||||||
|
|
||||||
|
allMatches.forEach(match => {
|
||||||
|
const existing = uniqueParents.get(match.parentLabel);
|
||||||
|
if (!existing || match.pathLength > existing.longestPathLength) {
|
||||||
|
uniqueParents.set(match.parentLabel, {
|
||||||
|
parentLabel: match.parentLabel,
|
||||||
|
longestPathLength: match.pathLength
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add only the parents with the most specific matches
|
||||||
|
newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
|
||||||
|
|
||||||
|
// Additionally, if no specific submenu match was found but the current path
|
||||||
|
// starts with one of the parent menu links, add that parent
|
||||||
|
if (newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPageGlobal = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Only open parent menu if the current path is a detail page of the most relevant parent
|
||||||
|
if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V3;
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
// if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
// console.log('🔍 Pattern Match Check:', {
|
||||||
|
// currentPath,
|
||||||
|
// detailPattern,
|
||||||
|
// regex: patternRegex.toString(),
|
||||||
|
// isMatch
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasCommonWord = segments.some(segment =>
|
||||||
|
commonWords.includes(segment.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hanya anggap sebagai detail page jika mengandung commonWords
|
||||||
|
return hasCommonWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = currentPath.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Calculate all submenu active states for conflict resolution
|
||||||
|
const submenuActiveStates = item.links?.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Determine if any submenu is active considering conflicts
|
||||||
|
const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
|
||||||
|
if (!isSubActive) return false;
|
||||||
|
|
||||||
|
// Check if there's a more specific match elsewhere
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
return other.isActive && other.pathLength > pathLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isSubActive && !hasMoreSpecificMatch;
|
||||||
|
}) || false;
|
||||||
|
|
||||||
|
// For parent menu detection, if current path contains common words,
|
||||||
|
// check if this parent menu's link is a prefix of the current path
|
||||||
|
const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
|
||||||
|
|
||||||
|
// Determine if this is the most relevant parent menu for the current path
|
||||||
|
const isMostRelevantParent = isParentOfDetailPage && (() => {
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
let mostRelevantParent = null;
|
||||||
|
|
||||||
|
// Find the parent with the longest matching prefix
|
||||||
|
items.forEach(parentItem => {
|
||||||
|
if (parentItem.links && parentItem.links.length > 0) {
|
||||||
|
parentItem.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
mostRelevantParent = parentItem.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mostRelevantParent === item.label;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
|
||||||
|
const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
|
||||||
|
|
||||||
|
// NEW LOGIC: If user is on a detail page that belongs to this parent menu,
|
||||||
|
// activate only the parent menu (open dropdown) without activating any submenu
|
||||||
|
const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
|
||||||
|
item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return currentPath.startsWith(linkPath + "/");
|
||||||
|
}) &&
|
||||||
|
!isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
|
||||||
|
const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
|
||||||
|
// then it should not be highlighted even if it would normally be the most relevant
|
||||||
|
const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
|
||||||
|
|
||||||
|
// NEW LOGIC: If this is an unlisted page, no menu should be highlighted
|
||||||
|
const isUnlistedPageAndNotRelevant = isUnlistedPage;
|
||||||
|
|
||||||
|
// FINAL LOGIC: Only activate this menu if:
|
||||||
|
// 1. It's the exact match for current path, OR
|
||||||
|
// 2. It's the most relevant parent, OR
|
||||||
|
// 3. It's a detail page of this menu
|
||||||
|
// But NOT if we're on a detail page and this isn't the relevant parent
|
||||||
|
// And NOT if this is an unlisted page
|
||||||
|
const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isActuallyRelevant && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isActuallyRelevant && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
// if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
// console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
// dashboardLink: subItem.link,
|
||||||
|
// dashboardLength: pathLength,
|
||||||
|
// otherLabel: other.subItem.label,
|
||||||
|
// otherLink: other.subItem.link,
|
||||||
|
// otherPattern: other.subItem.detailPattern,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// otherIsActive: other.isActive,
|
||||||
|
// isOtherLonger,
|
||||||
|
// willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
// if (isSubActive && other.isActive) {
|
||||||
|
// console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
// current: subItem.label,
|
||||||
|
// currentPath: subItem.link,
|
||||||
|
// currentLength: pathLength,
|
||||||
|
// other: other.subItem.label,
|
||||||
|
// otherPath: other.subItem.link,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// isOtherLonger,
|
||||||
|
// shouldDisableCurrent: isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
|
||||||
|
// Also don't highlight if this is an unlisted page
|
||||||
|
const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
// if (isSubActive) {
|
||||||
|
// console.log('✅ Active check:', {
|
||||||
|
// label: subItem.label,
|
||||||
|
// link: subItem.link,
|
||||||
|
// isSubActive,
|
||||||
|
// hasMoreSpecificMatch,
|
||||||
|
// finalIsActive,
|
||||||
|
// shouldHighlight,
|
||||||
|
// isOnDetailPage,
|
||||||
|
// isUnlistedPage
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={shouldHighlight ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
shouldHighlight && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
|
|||||||
|
|
||||||
const fcmToken = await getToken(messagingInstance);
|
const fcmToken = await getToken(messagingInstance);
|
||||||
if (!fcmToken) {
|
if (!fcmToken) {
|
||||||
console.warn("Tidak bisa mendapatkan FCM token");
|
console.log("Tidak bisa mendapatkan FCM token");
|
||||||
// logout();
|
// logout();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ FCM Token:", fcmToken);
|
console.log("✅ FCM Token:", fcmToken);
|
||||||
|
|||||||
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>
|
<Grid
|
||||||
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
|
// containerStyle={{
|
||||||
|
// bottom: 0,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<Grid.Col
|
||||||
|
span={rightComponent ? 6 : 12}
|
||||||
|
style={{ justifyContent: "center" }}
|
||||||
|
>
|
||||||
<TextCustom
|
<TextCustom
|
||||||
// style={{ alignSelf: "center" }}
|
// style={{ alignSelf: "center" }}
|
||||||
bold
|
bold
|
||||||
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</BaseBox>
|
</View>
|
||||||
|
{/* </BaseBox> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
export default function BasicWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -101,12 +101,14 @@ const NewWrapper = (props: NewWrapperProps) => {
|
|||||||
renderItem={listProps.renderItem}
|
renderItem={listProps.renderItem}
|
||||||
keyExtractor={
|
keyExtractor={
|
||||||
listProps.keyExtractor ||
|
listProps.keyExtractor ||
|
||||||
((item) => {
|
((item, index) => {
|
||||||
if (item.id == null) {
|
if (item.id == null) {
|
||||||
console.warn("Item tanpa 'id':", item);
|
console.warn("Item tanpa 'id':", item);
|
||||||
return `fallback-${JSON.stringify(item)}`;
|
return `fallback-${index}-${JSON.stringify(item)}`;
|
||||||
}
|
}
|
||||||
return String(item.id);
|
|
||||||
|
// Gabungkan ID dengan indeks untuk mencegah duplikasi
|
||||||
|
return `${String(item.id)}-${index}`;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
|
|||||||
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
||||||
import GridComponentView from "./_ShareComponent/GridSectionView";
|
import GridComponentView from "./_ShareComponent/GridSectionView";
|
||||||
import NewWrapper from "./_ShareComponent/NewWrapper";
|
import NewWrapper from "./_ShareComponent/NewWrapper";
|
||||||
|
import BasicWrapper from "./_ShareComponent/BasicWrapper";
|
||||||
// Progress
|
// Progress
|
||||||
import ProgressCustom from "./Progress/ProgressCustom";
|
import ProgressCustom from "./Progress/ProgressCustom";
|
||||||
// Loader
|
// Loader
|
||||||
@@ -121,6 +122,7 @@ export {
|
|||||||
GridComponentView,
|
GridComponentView,
|
||||||
Spacing,
|
Spacing,
|
||||||
NewWrapper,
|
NewWrapper,
|
||||||
|
BasicWrapper,
|
||||||
// Stack
|
// Stack
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TabBarBackground,
|
TabBarBackground,
|
||||||
|
|||||||
177
docs/admin-folder-structure.md
Normal file
177
docs/admin-folder-structure.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Struktur Folder Admin Aplikasi HIPMI Mobile
|
||||||
|
|
||||||
|
Dokumen ini menjelaskan struktur folder dan file untuk bagian admin dari aplikasi HIPMI Mobile yang terletak di `app/(application)/admin`.
|
||||||
|
|
||||||
|
## File dan Folder Tingkat Atas
|
||||||
|
|
||||||
|
### Folder
|
||||||
|
- `app-information` - Manajemen informasi aplikasi
|
||||||
|
- `collaboration` - Manajemen modul kolaborasi
|
||||||
|
- `donation` - Manajemen modul donasi
|
||||||
|
- `event` - Manajemen modul acara
|
||||||
|
- `forum` - Manajemen modul forum
|
||||||
|
- `investment` - Manajemen modul investasi
|
||||||
|
- `job` - Manajemen modul lowongan kerja
|
||||||
|
- `notification` - Manajemen notifikasi
|
||||||
|
- `super-admin` - Fungsi super admin
|
||||||
|
- `user-access` - Manajemen akses pengguna
|
||||||
|
- `voting` - Manajemen modul voting
|
||||||
|
|
||||||
|
### File
|
||||||
|
- `_layout.tsx` - Komponen tata letak untuk bagian admin
|
||||||
|
- `dashboard.tsx` - Tampilan dasbor admin
|
||||||
|
- `maps.tsx` - Fungsionalitas peta untuk admin
|
||||||
|
|
||||||
|
## Struktur Folder Terperinci
|
||||||
|
|
||||||
|
### app-information/
|
||||||
|
```
|
||||||
|
app-information/
|
||||||
|
├── business-field/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ ├── bidang-update.tsx
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── sub-bidang-update.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
├── information-bank/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
├── sticker/
|
||||||
|
│ ├── [id]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ └── create.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### collaboration/
|
||||||
|
```
|
||||||
|
collaboration/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status].tsx
|
||||||
|
│ ├── group.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── group.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── publish.tsx
|
||||||
|
└── reject.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### donation/
|
||||||
|
```
|
||||||
|
donation/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── transaction-detail.tsx
|
||||||
|
│ ├── detail-disbursement-of-funds.tsx
|
||||||
|
│ ├── disbursement-of-funds.tsx
|
||||||
|
│ ├── list-disbursement-of-funds.tsx
|
||||||
|
│ ├── list-of-donatur.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── category-create.tsx
|
||||||
|
├── category-update.tsx
|
||||||
|
├── category.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### event/
|
||||||
|
```
|
||||||
|
event/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ ├── list-of-participants.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── type-create.tsx
|
||||||
|
├── type-of-event.tsx
|
||||||
|
└── type-update.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### forum/
|
||||||
|
```
|
||||||
|
forum/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── index.tsx
|
||||||
|
│ ├── list-comment.tsx
|
||||||
|
│ ├── list-report-comment.tsx
|
||||||
|
│ └── list-report-posting.tsx
|
||||||
|
├── index.tsx
|
||||||
|
├── posting.tsx
|
||||||
|
├── report-comment.tsx
|
||||||
|
└── report-posting.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### investment/
|
||||||
|
```
|
||||||
|
investment/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── transaction-detail.tsx
|
||||||
|
│ ├── list-of-investor.tsx
|
||||||
|
│ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### job/
|
||||||
|
```
|
||||||
|
job/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### notification/
|
||||||
|
```
|
||||||
|
notification/
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### super-admin/
|
||||||
|
```
|
||||||
|
super-admin/
|
||||||
|
├── [id]/
|
||||||
|
│ └── index.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### user-access/
|
||||||
|
```
|
||||||
|
user-access/
|
||||||
|
├── [id]/
|
||||||
|
│ └── index.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### voting/
|
||||||
|
```
|
||||||
|
voting/
|
||||||
|
├── [id]/
|
||||||
|
│ ├── [status]/
|
||||||
|
│ │ ├── index.tsx
|
||||||
|
│ │ └── reject-input.tsx
|
||||||
|
├── [status]/
|
||||||
|
│ └── status.tsx
|
||||||
|
├── history.tsx
|
||||||
|
└── index.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rute Dinamis
|
||||||
|
|
||||||
|
Bagian admin menggunakan rute dinamis yang ditunjukkan dengan kurung siku `[ ]`:
|
||||||
|
- `[id]` - Rute dinamis untuk ID item tertentu
|
||||||
|
- `[status]` - Rute dinamis untuk tampilan berdasarkan status
|
||||||
|
|
||||||
|
Ini memungkinkan routing yang fleksibel berdasarkan parameter tertentu seperti ID item atau status.
|
||||||
@@ -1,8 +1,36 @@
|
|||||||
<!-- Start Penerapan Pagination -->
|
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
|
||||||
|
|
||||||
File utama: screens/Notification/ScreenNotification.tsx
|
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
|
||||||
Fun fecth: apiGetNotificationsById
|
Folder tujuan: screens/Donation
|
||||||
File fetch: service/api-notifications.ts
|
Nama file utama: ScreenFundDisbursement.tsx
|
||||||
|
|
||||||
|
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenFundDisbursement" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||||
|
Selanjutnya terapkan pagination pada file "Nama file utama"
|
||||||
|
|
||||||
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
|
File function fetch: service/api-client/api-donation.ts
|
||||||
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
|
Terapkan pagination pada file "Nama file utama"
|
||||||
|
Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
|
||||||
|
|
||||||
|
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||||
|
|
||||||
|
Perbaiki fetch "Function fecth" , pada file "File function fetch"
|
||||||
|
Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
||||||
|
|
||||||
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||||
|
|
||||||
|
<!-- Additional Prompt -->
|
||||||
|
File refrensi: screens/Donation/ScreenListOfNews.tsx
|
||||||
|
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan Pagination ` ===================== -->
|
||||||
|
|
||||||
|
<!-- ===================== Start Penerapan NewWrapper & Pagination ===================== -->
|
||||||
|
File utama: screens/Donation/ScreenFundDisbursement.tsx
|
||||||
|
Function fecth: apiDonationDisbursementOfFundsListById
|
||||||
|
File function fetch: service/api-client/api-donation.ts
|
||||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
|
||||||
Terapkan pagination pada file "File utama"
|
Terapkan pagination pada file "File utama"
|
||||||
@@ -10,13 +38,70 @@ Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "Fi
|
|||||||
|
|
||||||
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||||
|
|
||||||
Perbaiki fetch "Fun fecth" , pada file "File fetch"
|
Perbaiki fetch "Function fecth" , pada file "File function fetch"
|
||||||
Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
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 Pagination -->
|
|
||||||
|
<!-- Additinal prompt -->
|
||||||
|
|
||||||
|
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
|
||||||
|
|
||||||
<!-- Start Penerapan NewWrapper -->
|
<!-- Start Penerapan NewWrapper -->
|
||||||
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
|
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
||||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
|
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||||
|
<!-- End Penerapan NewWrapper -->
|
||||||
|
|
||||||
|
<!-- Start Random Prompt -->
|
||||||
|
|
||||||
|
|
||||||
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar
|
||||||
|
<!-- End Random Prompt -->
|
||||||
|
|
||||||
|
<!-- START Prompt Admin Refactoring -->
|
||||||
|
<!-- Pindah kode ke Screen Component -->
|
||||||
|
File source: app/(application)/admin/app-information/business-field/[id]/index.tsx
|
||||||
|
Folder tujuan: screens/Admin/App-Information
|
||||||
|
Nama file utama: ScreenBusinessFieldDetail.tsx
|
||||||
|
Nama function utama: Admin_ScreenBusinessFieldDetail
|
||||||
|
File komponen wrapper: components/_ShareComponent/NewWrapper.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"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Penerapan Pagination -->
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||||
|
<!-- END Prompt Admin Refactoring -->
|
||||||
|
|
||||||
|
<!-- 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 )
|
||||||
|
<!-- Baru -->
|
||||||
|
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
|
||||||
|
<!-- END Use Prompt Now -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
onPress={() =>
|
||||||
|
router.push(`/admin/app-information/business-field/${item.item.id}`)
|
||||||
|
}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col span={2} style={{ alignItems: "center" }}>
|
<Grid.Col span={8} style={{ alignSelf: "center" }}>
|
||||||
<TextCustom bold>Aksi</TextCustom>
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
{item?.item?.name || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={4} style={{ alignItems: "center" }}>
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
<TextCustom bold>Status</TextCustom>
|
<CenterCustom>
|
||||||
</Grid.Col>
|
{item?.item?.active ? (
|
||||||
<Grid.Col span={6}>
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
<TextCustom bold>Nama Bidang Bisnis</TextCustom>
|
) : (
|
||||||
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
|
)}
|
||||||
|
</CenterCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</AdminBasicBox>
|
||||||
<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={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${item.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={4}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={
|
|
||||||
item.active ? AccentColor.blue : AccentColor.blackgray
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
<TextCustom>{item.name}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</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
|
||||||
|
onPress={() =>
|
||||||
|
router.push(`/admin/app-information/information-bank/${item.item.id}`)
|
||||||
|
}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col span={3}>
|
<Grid.Col span={8}>
|
||||||
<TextCustom bold align="center">
|
<StackCustom gap={"xs"}>
|
||||||
Aksi
|
<TextCustom bold truncate>
|
||||||
</TextCustom>
|
{item?.item?.namaBank || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size={"small"} bold truncate color="gray">
|
||||||
|
{item?.item?.norek || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={3}>
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
<TextCustom bold align="center">
|
<CenterCustom>
|
||||||
Status
|
{item?.item?.isActive ? (
|
||||||
</TextCustom>
|
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||||
</Grid.Col>
|
) : (
|
||||||
<Grid.Col span={6}>
|
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||||
<TextCustom bold align="center">
|
)}
|
||||||
Nama Bank
|
</CenterCustom>
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</AdminBasicBox>
|
||||||
<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={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/information-bank/${item.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={3}
|
|
||||||
style={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
>
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={
|
|
||||||
item.isActive
|
|
||||||
? AccentColor.blue
|
|
||||||
: AccentColor.blackgray
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
<TextCustom align="center">{item.namaBank}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal file
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import {
|
||||||
|
AlertDefaultSystem,
|
||||||
|
BackButton,
|
||||||
|
BaseBox,
|
||||||
|
DrawerCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
|
ScrollableCustom,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||||
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
|
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
|
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||||
|
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||||
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
const selectedCategory = (value: string) => {
|
||||||
|
const category = listOfcategoriesAppNotification.find(
|
||||||
|
(c) => c.value === value
|
||||||
|
);
|
||||||
|
return category?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoxNotification = ({
|
||||||
|
data,
|
||||||
|
activeCategory,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
activeCategory: string | null;
|
||||||
|
}) => {
|
||||||
|
const { markAsRead } = useNotificationStore();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox
|
||||||
|
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||||
|
onPress={() => {
|
||||||
|
console.log(
|
||||||
|
"Notification >",
|
||||||
|
selectedCategory(activeCategory as string)
|
||||||
|
);
|
||||||
|
router.push(data.deepLink);
|
||||||
|
markAsRead(data.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2} bold>
|
||||||
|
{data.title}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom size="small" color="gray">
|
||||||
|
{formatChatTime(data.createdAt)}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Admin_ScreenNotification() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||||
|
const [listData, setListData] = useState<any[]>([]);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const { markAsReadAll } = useNotificationStore();
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
// tambahkan logika lain seperti filter dsb.
|
||||||
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fecthData();
|
||||||
|
}, [activeCategory])
|
||||||
|
);
|
||||||
|
|
||||||
|
const fecthData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiGetNotificationsById({
|
||||||
|
id: user?.id as any,
|
||||||
|
category: activeCategory as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
setListData(response.data);
|
||||||
|
} else {
|
||||||
|
setListData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error Notification", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
fecthData();
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Admin Notifikasi",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => (
|
||||||
|
<IconDot
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => setOpenDrawer(true)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewWrapper
|
||||||
|
headerComponent={
|
||||||
|
<ScrollableCustom
|
||||||
|
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ListSkeletonComponent />
|
||||||
|
) : _.isEmpty(listData) ? (
|
||||||
|
<NoDataText text="Belum ada notifikasi" />
|
||||||
|
) : (
|
||||||
|
listData.map((e, i) => (
|
||||||
|
<View key={i}>
|
||||||
|
<BoxNotification
|
||||||
|
data={e}
|
||||||
|
activeCategory={activeCategory as any}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</NewWrapper>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: "Tandai Semua Dibaca",
|
||||||
|
value: "read-all",
|
||||||
|
icon: (
|
||||||
|
<Ionicons
|
||||||
|
name="reader-outline"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color={MainColor.white}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
path: "",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item: any) => {
|
||||||
|
console.log("Item", item.value);
|
||||||
|
if (item.value === "read-all") {
|
||||||
|
AlertDefaultSystem({
|
||||||
|
title: "Tandai Semua Dibaca",
|
||||||
|
message:
|
||||||
|
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||||
|
textLeft: "Batal",
|
||||||
|
textRight: "Ya",
|
||||||
|
onPressRight: () => {
|
||||||
|
markAsReadAll(user?.id as any);
|
||||||
|
const data = _.cloneDeep(listData);
|
||||||
|
data.forEach((e) => {
|
||||||
|
e.isRead = true;
|
||||||
|
});
|
||||||
|
setListData(data);
|
||||||
|
onRefresh();
|
||||||
|
setOpenDrawer(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
AlertDefaultSystem,
|
||||||
|
BackButton,
|
||||||
|
BaseBox,
|
||||||
|
DrawerCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
NewWrapper,
|
||||||
|
ScrollableCustom,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||||
|
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||||
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
const selectedCategory = (value: string) => {
|
||||||
|
const category = listOfcategoriesAppNotification.find(
|
||||||
|
(c) => c.value === value,
|
||||||
|
);
|
||||||
|
return category?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fixPath = ({
|
||||||
|
deepLink,
|
||||||
|
categoryApp,
|
||||||
|
}: {
|
||||||
|
deepLink: string;
|
||||||
|
categoryApp: string;
|
||||||
|
}) => {
|
||||||
|
// Jika categoryApp adalah "OTHER", kembalikan deepLink tanpa perubahan
|
||||||
|
if (categoryApp === "OTHER") {
|
||||||
|
return deepLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika dalam deepLink terdapat "/admin/", kembalikan path tersebut tanpa modifikasi tambahan
|
||||||
|
if (deepLink.includes("/admin/")) {
|
||||||
|
return deepLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Category App", categoryApp);
|
||||||
|
console.log("Deep Link", deepLink);
|
||||||
|
|
||||||
|
const separator = deepLink.includes("?") ? "&" : "?";
|
||||||
|
|
||||||
|
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||||
|
categoryApp,
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
console.log("Fix Path", fixedPath);
|
||||||
|
|
||||||
|
return fixedPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoxNotification = ({
|
||||||
|
data,
|
||||||
|
activeCategory,
|
||||||
|
setListData,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
activeCategory: string | null;
|
||||||
|
setListData: (data: any) => void;
|
||||||
|
}) => {
|
||||||
|
const { markAsRead } = useNotificationStore();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox
|
||||||
|
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||||
|
onPress={() => {
|
||||||
|
const newPath = fixPath({
|
||||||
|
deepLink: data.deepLink,
|
||||||
|
categoryApp: data.kategoriApp,
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedCategory(activeCategory as string);
|
||||||
|
router.navigate(newPath as any);
|
||||||
|
|
||||||
|
if (!data.isRead) {
|
||||||
|
markAsRead(data.id);
|
||||||
|
setListData((prev: any) =>
|
||||||
|
prev.map((item: any) =>
|
||||||
|
item.id === data.id ? { ...item, isRead: true } : item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2} bold>
|
||||||
|
{data.title}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom size="small" color="gray">
|
||||||
|
{formatChatTime(data.createdAt)}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Admin_ScreenNotification2() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const { markAsReadAll } = useNotificationStore();
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
if (!user?.id) return { data: [] };
|
||||||
|
|
||||||
|
return await apiGetNotificationsById({
|
||||||
|
id: user?.id as any,
|
||||||
|
category: activeCategory as any,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [user?.id, activeCategory],
|
||||||
|
onError: (error) =>
|
||||||
|
console.error("[ERROR] Fetch admin notifications:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada notifikasi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item notification
|
||||||
|
const renderNotificationItem = ({ item }: { item: any }) => (
|
||||||
|
<View key={item.id}>
|
||||||
|
<BoxNotification
|
||||||
|
data={item}
|
||||||
|
activeCategory={activeCategory as any}
|
||||||
|
setListData={pagination.setListData}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
// Reset pagination saat kategori berubah
|
||||||
|
pagination.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Admin Notifikasi",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => (
|
||||||
|
<IconDot
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => setOpenDrawer(true)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewWrapper
|
||||||
|
headerComponent={
|
||||||
|
<ScrollableCustom
|
||||||
|
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderNotificationItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: "Tandai Semua Dibaca",
|
||||||
|
value: "read-all",
|
||||||
|
icon: (
|
||||||
|
<Ionicons
|
||||||
|
name="reader-outline"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color={MainColor.white}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
path: "",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item: any) => {
|
||||||
|
console.log("Item", item.value);
|
||||||
|
if (item.value === "read-all") {
|
||||||
|
AlertDefaultSystem({
|
||||||
|
title: "Tandai Semua Dibaca",
|
||||||
|
message:
|
||||||
|
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||||
|
textLeft: "Batal",
|
||||||
|
textRight: "Ya",
|
||||||
|
onPressRight: () => {
|
||||||
|
markAsReadAll(user?.id as any);
|
||||||
|
const data = _.cloneDeep(pagination.listData);
|
||||||
|
data.forEach((e) => {
|
||||||
|
e.isRead = true;
|
||||||
|
});
|
||||||
|
pagination.setListData(data);
|
||||||
|
pagination.onRefresh();
|
||||||
|
setOpenDrawer(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
461
screens/Admin/listPageAdmin_V2.tsx
Normal file
461
screens/Admin/listPageAdmin_V2.tsx
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2";
|
||||||
|
|
||||||
|
|
||||||
|
export { adminListMenu_V2, superAdminListMenu_V2 }
|
||||||
|
|
||||||
|
const adminListMenu_V2: NavbarItem_V2[] = [
|
||||||
|
{
|
||||||
|
label: "Main Dashboard",
|
||||||
|
icon: "home",
|
||||||
|
link: "/admin/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Investasi",
|
||||||
|
icon: "wallet",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/investment",
|
||||||
|
// Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/investment/publish/status",
|
||||||
|
detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/investment/review/status",
|
||||||
|
detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/investment/reject/status",
|
||||||
|
detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Donasi",
|
||||||
|
icon: "hand-right",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/donation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/donation/publish/status",
|
||||||
|
detailPattern: "/admin/donation/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/donation/review/status",
|
||||||
|
detailPattern: "/admin/donation/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/donation/reject/status",
|
||||||
|
detailPattern: "/admin/donation/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori",
|
||||||
|
link: "/admin/donation/category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Event",
|
||||||
|
icon: "calendar-clear",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/event/publish/status",
|
||||||
|
detailPattern: "/admin/event/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/event/review/status",
|
||||||
|
detailPattern: "/admin/event/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/event/reject/status",
|
||||||
|
detailPattern: "/admin/event/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tipe Acara",
|
||||||
|
link: "/admin/event/type-of-event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/event/history/status",
|
||||||
|
detailPattern: "/admin/event/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Voting",
|
||||||
|
icon: "accessibility-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/voting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/voting/publish/status",
|
||||||
|
detailPattern: "/admin/voting/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/voting/review/status",
|
||||||
|
detailPattern: "/admin/voting/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/voting/reject/status",
|
||||||
|
detailPattern: "/admin/voting/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/voting/history",
|
||||||
|
detailPattern: "/admin/voting/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Job",
|
||||||
|
icon: "desktop-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/job/publish/status",
|
||||||
|
detailPattern: "/admin/job/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/job/review/status",
|
||||||
|
detailPattern: "/admin/job/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/job/reject/status",
|
||||||
|
detailPattern: "/admin/job/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Forum",
|
||||||
|
icon: "chatbubble-ellipses-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/forum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Posting",
|
||||||
|
link: "/admin/forum/posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Posting",
|
||||||
|
link: "/admin/forum/report-posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Komentar",
|
||||||
|
link: "/admin/forum/report-comment",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Collaboration",
|
||||||
|
icon: "people",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/collaboration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/collaboration/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Group",
|
||||||
|
link: "/admin/collaboration/group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/collaboration/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Maps",
|
||||||
|
icon: "map",
|
||||||
|
link: "/admin/maps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "App Information",
|
||||||
|
icon: "information-circle",
|
||||||
|
link: "/admin/app-information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User Access",
|
||||||
|
icon: "people",
|
||||||
|
link: "/admin/user-access",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const superAdminListMenu_V2: NavbarItem_V2[] = [
|
||||||
|
{
|
||||||
|
label: "Main Dashboard",
|
||||||
|
icon: "home",
|
||||||
|
link: "/admin/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Investasi",
|
||||||
|
icon: "wallet",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/investment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/investment/publish/status",
|
||||||
|
detailPattern: "/admin/investment/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/investment/review/status",
|
||||||
|
detailPattern: "/admin/investment/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/investment/reject/status",
|
||||||
|
detailPattern: "/admin/investment/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Donasi",
|
||||||
|
icon: "hand-right",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/donation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/donation/publish/status",
|
||||||
|
detailPattern: "/admin/donation/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/donation/review/status",
|
||||||
|
detailPattern: "/admin/donation/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/donation/reject/status",
|
||||||
|
detailPattern: "/admin/donation/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori",
|
||||||
|
link: "/admin/donation/category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Event",
|
||||||
|
icon: "calendar-clear",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/event/publish/status",
|
||||||
|
detailPattern: "/admin/event/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/event/review/status",
|
||||||
|
detailPattern: "/admin/event/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/event/reject/status",
|
||||||
|
detailPattern: "/admin/event/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tipe Acara",
|
||||||
|
link: "/admin/event/type-of-event",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/event/history/status",
|
||||||
|
detailPattern: "/admin/event/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Voting",
|
||||||
|
icon: "accessibility-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/voting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/voting/publish/status",
|
||||||
|
detailPattern: "/admin/voting/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/voting/review/status",
|
||||||
|
detailPattern: "/admin/voting/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/voting/reject/status",
|
||||||
|
detailPattern: "/admin/voting/*/reject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Riwayat",
|
||||||
|
link: "/admin/voting/history",
|
||||||
|
detailPattern: "/admin/voting/*/history",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Job",
|
||||||
|
icon: "desktop-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/job/publish/status",
|
||||||
|
detailPattern: "/admin/job/*/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Review",
|
||||||
|
link: "/admin/job/review/status",
|
||||||
|
detailPattern: "/admin/job/*/review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/job/reject/status",
|
||||||
|
detailPattern: "/admin/job/*/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Forum",
|
||||||
|
icon: "chatbubble-ellipses-outline",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/forum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Posting",
|
||||||
|
link: "/admin/forum/posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Posting",
|
||||||
|
link: "/admin/forum/report-posting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Komentar",
|
||||||
|
link: "/admin/forum/report-comment",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Collaboration",
|
||||||
|
icon: "people",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
link: "/admin/collaboration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Publish",
|
||||||
|
link: "/admin/collaboration/publish",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Group",
|
||||||
|
link: "/admin/collaboration/group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reject",
|
||||||
|
link: "/admin/collaboration/reject",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Maps",
|
||||||
|
icon: "map",
|
||||||
|
link: "/admin/maps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "App Information",
|
||||||
|
icon: "information-circle",
|
||||||
|
link: "/admin/app-information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User Access",
|
||||||
|
icon: "people",
|
||||||
|
link: "/admin/user-access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Super Admin",
|
||||||
|
icon: "globe",
|
||||||
|
link: "/admin/super-admin",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
=================================================================================
|
||||||
|
PENJELASAN detailPattern:
|
||||||
|
=================================================================================
|
||||||
|
|
||||||
|
detailPattern digunakan untuk match dengan URL detail page yang strukturnya:
|
||||||
|
/admin/{module}/[id]/[status]
|
||||||
|
|
||||||
|
Contoh untuk Job Review:
|
||||||
|
- Link: /admin/job/review/status (halaman list review)
|
||||||
|
- detailPattern: /admin/job/* /review (detail dari review)
|
||||||
|
- Match dengan: /admin/job/123/review, /admin/job/456/review, dll
|
||||||
|
|
||||||
|
Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric).
|
||||||
|
|
||||||
|
Modul yang PERLU detailPattern:
|
||||||
|
✅ Investasi - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
✅ Donasi - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status])
|
||||||
|
✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status])
|
||||||
|
✅ Job - Publish, Review, Reject (ada [id]/[status])
|
||||||
|
|
||||||
|
Modul yang TIDAK PERLU detailPattern:
|
||||||
|
❌ Forum - posting, report-posting, report-comment (struktur berbeda)
|
||||||
|
❌ Collaboration - struktur berbeda
|
||||||
|
❌ Maps, App Information, User Access - single page
|
||||||
|
❌ Dashboard submenu - auto-match dengan parent path
|
||||||
|
|
||||||
|
=================================================================================
|
||||||
|
*/
|
||||||
21
screens/Donation/BoxNews.tsx
Normal file
21
screens/Donation/BoxNews.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
|
||||||
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
|
|
||||||
|
export default function Donation_BoxNews({item}: {item: any}){
|
||||||
|
return <>
|
||||||
|
<BaseBox href={`/donation/[id]/(news)/${item?.id}`}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom truncate bold>
|
||||||
|
{item?.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<TextCustom size="small">
|
||||||
|
{formatChatTime(item?.createdAt)}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ export default function Donation_ButtonStatusSection({
|
|||||||
}) {
|
}) {
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [isLoadingDelete, setLoadingDelete] = useState(false);
|
const [isLoadingDelete, setLoadingDelete] = useState(false);
|
||||||
|
const path: any = (status: string) => {
|
||||||
|
return `/donation/(tabs)/status?status=${status}`;
|
||||||
|
};
|
||||||
const handleBatalkanReview = async () => {
|
const handleBatalkanReview = async () => {
|
||||||
AlertDefaultSystem({
|
AlertDefaultSystem({
|
||||||
title: "Batalkan Review",
|
title: "Batalkan Review",
|
||||||
@@ -43,7 +46,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.push(path("draft"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -80,7 +83,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.replace(path("review"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -117,7 +120,7 @@ export default function Donation_ButtonStatusSection({
|
|||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.replace(path("draft"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR]", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import { View } from "react-native";
|
|||||||
export default function Donation_ComponentBoxDetailData({
|
export default function Donation_ComponentBoxDetailData({
|
||||||
bottomSection,
|
bottomSection,
|
||||||
data,
|
data,
|
||||||
|
showSisaHari = true,
|
||||||
sisaHari,
|
sisaHari,
|
||||||
reminder,
|
reminder,
|
||||||
}: {
|
}: {
|
||||||
bottomSection?: React.ReactNode;
|
bottomSection?: React.ReactNode;
|
||||||
data: any;
|
data: any;
|
||||||
|
showSisaHari?: boolean;
|
||||||
sisaHari: number;
|
sisaHari: number;
|
||||||
reminder: boolean;
|
reminder: boolean;
|
||||||
}) {
|
}) {
|
||||||
@@ -34,9 +36,9 @@ export default function Donation_ComponentBoxDetailData({
|
|||||||
<TextCustom bold color="red">
|
<TextCustom bold color="red">
|
||||||
Waktu berakhir
|
Waktu berakhir
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
) : (
|
) : showSisaHari ? (
|
||||||
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
|
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
|
||||||
)}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
62
screens/Donation/ScreenBeranda.tsx
Normal file
62
screens/Donation/ScreenBeranda.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import FloatingButton from "@/components/Button/FloatingButton";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
|
||||||
|
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
export default function Donation_ScreenBeranda() {
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, search) => {
|
||||||
|
return await apiDonationGetAll({
|
||||||
|
category: "beranda",
|
||||||
|
page: String(page),
|
||||||
|
}).then((res) => {
|
||||||
|
console.log("RES", JSON.stringify(res, null, 2));
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
|
||||||
|
dependencies: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada donasi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Donation_BoxPublish data={item} id={item.id} />
|
||||||
|
)}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton onPress={() => router.push("/donation/create")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
174
screens/Donation/ScreenFundDisbursement.tsx
Normal file
174
screens/Donation/ScreenFundDisbursement.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
BaseBox,
|
||||||
|
Grid,
|
||||||
|
InformationBox,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import {
|
||||||
|
apiDonationDisbursementOfFundsListById,
|
||||||
|
apiDonationGetOne,
|
||||||
|
} from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { Divider } from "react-native-paper";
|
||||||
|
|
||||||
|
interface Donation_ScreenFundDisbursementProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenFundDisbursement({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenFundDisbursementProps) {
|
||||||
|
const [data, setData] = useState({
|
||||||
|
totalPencairan: 0,
|
||||||
|
akumulasiPencairan: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ambil data utama (total pencairan, dll) terpisah dari pagination
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onLoadData = async () => {
|
||||||
|
try {
|
||||||
|
const responseData = await apiDonationGetOne({
|
||||||
|
id: donationId,
|
||||||
|
category: "permanent",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
setData({
|
||||||
|
totalPencairan: responseData.data.totalPencairan,
|
||||||
|
akumulasiPencairan: responseData.data.akumulasiPencairan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadData();
|
||||||
|
}, [donationId]);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationDisbursementOfFundsListById({
|
||||||
|
id: donationId,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BaseBox key={index}>
|
||||||
|
<StackCustom>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom bold>{item?.title}</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<TextCustom size="small">
|
||||||
|
{dayjs(item?.createdAt).format("DD MMM YYYY")}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<TextCustom>{item?.deskripsi}</TextCustom>
|
||||||
|
{/* <Spacing /> */}
|
||||||
|
<Divider />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8} style={{ alignSelf: "center" }}>
|
||||||
|
<TextCustom bold size={"large"}>
|
||||||
|
Rp. {formatCurrencyDisplay(item?.nominalCair)}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<ActionIcon
|
||||||
|
icon={<Feather name="file-text" color={MainColor.black} />}
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate(
|
||||||
|
`/(application)/(image)/preview-image/${item?.imageId}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
{/*
|
||||||
|
<ButtonCenteredOnly
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate(
|
||||||
|
`/(application)/(image)/preview-image/${item?.imageId}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
icon="file-text"
|
||||||
|
>
|
||||||
|
Bukti Transaksi
|
||||||
|
</ButtonCenteredOnly> */}
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Komponen header yang akan ditampilkan di atas daftar
|
||||||
|
const ListHeaderComponent = (
|
||||||
|
<View>
|
||||||
|
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
|
||||||
|
<BaseBox>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size="small">Total Pencairan Dana</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
{data?.akumulasiPencairan} kali
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
screens/Donation/ScreenListOfDonatur.tsx
Normal file
98
screens/Donation/ScreenListOfDonatur.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BaseBox,
|
||||||
|
Grid,
|
||||||
|
LoaderCustom,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationListOfDonaturById } from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { FontAwesome6 } from "@expo/vector-icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
|
||||||
|
interface Donation_ScreenListOfDonaturProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenListOfDonatur({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenListOfDonaturProps) {
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationListOfDonaturById({
|
||||||
|
id: donationId,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<BaseBox key={index}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col
|
||||||
|
span={3}
|
||||||
|
style={{ alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<FontAwesome6
|
||||||
|
name="face-smile-wink"
|
||||||
|
size={50}
|
||||||
|
style={{ color: MainColor.yellow }}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<TextCustom bold size="large">
|
||||||
|
{item?.Author?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<Spacing />
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
|
||||||
|
<TextCustom bold size="large" color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom>
|
||||||
|
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada donatur",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
screens/Donation/ScreenListOfNews.tsx
Normal file
97
screens/Donation/ScreenListOfNews.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { BackButton, DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
||||||
|
import { router, Stack } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Donation_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface Donation_ScreenListOfNewsProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenListOfNews({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenListOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetNewsById({
|
||||||
|
id: donationId,
|
||||||
|
category: "get-all",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donation_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada kabar",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Daftar Kabar",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
icon: <IconPlus />,
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/donation/${donationId}/(news)/add-news`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
console.log("PATH ", item.path);
|
||||||
|
router.navigate(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
screens/Donation/ScreenMyDonation.tsx
Normal file
152
screens/Donation/ScreenMyDonation.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
BadgeCustom,
|
||||||
|
BaseBox,
|
||||||
|
DummyLandscapeImage,
|
||||||
|
Grid,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import FloatingButton from "@/components/Button/FloatingButton";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationGetAll } from "@/service/api-client/api-donation";
|
||||||
|
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||||
|
import { Href, router } from "expo-router";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export default function Donation_ScreenMyDonation() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, search) => {
|
||||||
|
if (!user?.id) {
|
||||||
|
throw new Error("User tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await apiDonationGetAll({
|
||||||
|
category: "my-donation",
|
||||||
|
authorId: user?.id,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
|
||||||
|
dependencies: [user?.id], // Reload ketika user.id berubah
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Belum ada transaksi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlerColor = (status: string) => {
|
||||||
|
if (status === "menunggu") {
|
||||||
|
return "orange";
|
||||||
|
} else if (status === "proses") {
|
||||||
|
return "white";
|
||||||
|
} else if (status === "berhasil") {
|
||||||
|
return "green";
|
||||||
|
} else if (status === "gagal") {
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePress = ({
|
||||||
|
invoiceId,
|
||||||
|
donationId,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
invoiceId: string;
|
||||||
|
donationId: string;
|
||||||
|
status: string;
|
||||||
|
}) => {
|
||||||
|
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
|
||||||
|
if (status === "menunggu") {
|
||||||
|
router.push(`${url}/invoice`);
|
||||||
|
} else if (status === "proses") {
|
||||||
|
router.push(`${url}/process`);
|
||||||
|
} else if (status === "berhasil") {
|
||||||
|
router.push(`${url}/success`);
|
||||||
|
} else if (status === "gagal") {
|
||||||
|
router.push(`${url}/failed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: any }) => (
|
||||||
|
<BaseBox
|
||||||
|
paddingTop={7}
|
||||||
|
paddingBottom={7}
|
||||||
|
onPress={() => {
|
||||||
|
handlePress({
|
||||||
|
status: _.lowerCase(item.statusInvoice),
|
||||||
|
invoiceId: item.id,
|
||||||
|
donationId: item.donasiId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={5}>
|
||||||
|
<DummyLandscapeImage
|
||||||
|
height={100}
|
||||||
|
unClickPath
|
||||||
|
imageId={item.imageId}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={1}>
|
||||||
|
<View />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2} bold>
|
||||||
|
{item.title || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom bold color="yellow">
|
||||||
|
Rp. {formatCurrencyDisplay(item.nominal)}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<BadgeCustom
|
||||||
|
variant="light"
|
||||||
|
color={handlerColor(_.lowerCase(item.statusInvoice))}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{item.statusInvoice}
|
||||||
|
</BadgeCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton onPress={() => router.push("/donation/create")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
screens/Donation/ScreenRecapOfNews.tsx
Normal file
105
screens/Donation/ScreenRecapOfNews.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BackButton,
|
||||||
|
DotButton,
|
||||||
|
DrawerCustom,
|
||||||
|
MenuDrawerDynamicGrid,
|
||||||
|
} from "@/components";
|
||||||
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
import Donation_BoxNews from "./BoxNews";
|
||||||
|
|
||||||
|
interface Donation_ScreenRecapOfNewsProps {
|
||||||
|
donationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenRecapOfNews({
|
||||||
|
donationId,
|
||||||
|
}: Donation_ScreenRecapOfNewsProps) {
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetNewsById({
|
||||||
|
id: donationId,
|
||||||
|
category: "get-all",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [donationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [donationId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donation_BoxNews key={index} item={item} />
|
||||||
|
);
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: "Tidak ada kabar",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Rekap Kabar",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<MenuDrawerDynamicGrid
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
icon: <IconPlus />,
|
||||||
|
label: "Tambah Berita",
|
||||||
|
path: `/donation/${donationId}/(news)/add-news`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPressItem={(item) => {
|
||||||
|
console.log("PATH ", item.path);
|
||||||
|
router.navigate(item.path as any);
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
screens/Donation/ScreenStatus.tsx
Normal file
97
screens/Donation/ScreenStatus.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { ScrollableCustom } from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||||
|
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
|
||||||
|
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
|
||||||
|
import { useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
interface DonationStatusProps {
|
||||||
|
initialStatus?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Donation_ScreenStatus({
|
||||||
|
initialStatus = "publish",
|
||||||
|
}: DonationStatusProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||||
|
initialStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiDonationGetByStatus({
|
||||||
|
authorId: user?.id as string,
|
||||||
|
status: activeCategory as string,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||||
|
dependencies: [user?.id, activeCategory],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [user?.id, activeCategory]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
pagination.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollComponent = (
|
||||||
|
<ScrollableCustom
|
||||||
|
data={dummyMasterStatus.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as any}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||||
|
<Donasi_BoxStatus data={item} status={activeCategory as string} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
isInitialLoad: pagination.isInitialLoad,
|
||||||
|
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||||
|
skeletonCount: 5,
|
||||||
|
skeletonHeight: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideFooter
|
||||||
|
headerComponent={scrollComponent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AvatarUsernameAndOtherComponent,
|
AvatarUsernameAndOtherComponent,
|
||||||
BoxWithHeaderSection,
|
BoxWithHeaderSection,
|
||||||
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function Event_ButtonStatusSection({
|
|||||||
type: "success",
|
type: "success",
|
||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(`/event/(tabs)/status?status=draft`);
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "info",
|
type: "info",
|
||||||
@@ -65,7 +65,7 @@ export default function Event_ButtonStatusSection({
|
|||||||
type: "success",
|
type: "success",
|
||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(`/event/(tabs)/status?status=review`);
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "info",
|
type: "info",
|
||||||
@@ -99,7 +99,7 @@ export default function Event_ButtonStatusSection({
|
|||||||
type: "success",
|
type: "success",
|
||||||
text1: response.message,
|
text1: response.message,
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(`/event/(tabs)/status?status=draft`);
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "info",
|
type: "info",
|
||||||
|
|||||||
75
screens/Event/ScreenBeranda.tsx
Normal file
75
screens/Event/ScreenBeranda.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { TextCustom } from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import FloatingButton from "@/components/Button/FloatingButton";
|
||||||
|
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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||||
|
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||||
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
|
||||||
|
export default function Event_ScreenBeranda() {
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiEventGetAll({
|
||||||
|
category: "beranda",
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [],
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada event",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item event
|
||||||
|
const renderEventItem = ({ item }: { item: any }) => (
|
||||||
|
<Event_BoxPublishSection
|
||||||
|
key={item.id}
|
||||||
|
href={`/event/${item.id}/publish`}
|
||||||
|
data={item}
|
||||||
|
rightComponentAvatar={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderEventItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton onPress={() => router.push("/event/create")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
screens/Event/ScreenContribution.tsx
Normal file
102
screens/Event/ScreenContribution.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
AvatarUsernameAndOtherComponent,
|
||||||
|
BoxWithHeaderSection,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom
|
||||||
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||||
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
|
import { RefreshControl } from "react-native";
|
||||||
|
|
||||||
|
|
||||||
|
export default function Event_ScreenContribution() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
if (!user?.id) return { data: [] };
|
||||||
|
|
||||||
|
return await apiEventGetAll({
|
||||||
|
category: "contribution",
|
||||||
|
userId: user?.id,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [user?.id],
|
||||||
|
onError: (error) =>
|
||||||
|
console.error("[ERROR] Fetch event contribution:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada kontribusi",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item event
|
||||||
|
const renderEventItem = ({ item }: { item: any }) => (
|
||||||
|
<BoxWithHeaderSection
|
||||||
|
key={item?.id}
|
||||||
|
href={`/event/${item?.Event?.id}/contribution`}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<AvatarUsernameAndOtherComponent
|
||||||
|
avatar={item?.Event?.Author?.Profile?.imageId}
|
||||||
|
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
|
||||||
|
name={item?.Event?.Author?.username}
|
||||||
|
rightComponent={
|
||||||
|
<TextCustom truncate>
|
||||||
|
{dateTimeView({
|
||||||
|
date: item?.Event?.tanggal,
|
||||||
|
withoutTime: true,
|
||||||
|
})}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextCustom bold align="center" size="xlarge" truncate={2}>
|
||||||
|
{item?.Event?.title}
|
||||||
|
</TextCustom>
|
||||||
|
<Spacing height={0} />
|
||||||
|
</StackCustom>
|
||||||
|
</BoxWithHeaderSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// pagination.onRefresh();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderEventItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
screens/Event/ScreenHistory.tsx
Normal file
121
screens/Event/ScreenHistory.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { ButtonCustom, Spacing, TextCustom } from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { AccentColor, 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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||||
|
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||||
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export default function Event_ScreenHistory() {
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiEventGetAll({
|
||||||
|
category: activeCategory === "all" ? "all-history" : "my-history",
|
||||||
|
userId: user?.id,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [user?.id, activeCategory],
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch event history:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada riwayat",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item event
|
||||||
|
const renderEventItem = ({ item }: { item: any }) => (
|
||||||
|
<Event_BoxPublishSection
|
||||||
|
key={item && item?.id}
|
||||||
|
data={item}
|
||||||
|
rightComponentAvatar={
|
||||||
|
<TextCustom>
|
||||||
|
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||||
|
</TextCustom>
|
||||||
|
}
|
||||||
|
href={`/event/${item.id}/history`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item);
|
||||||
|
// Reset pagination saat kategori berubah
|
||||||
|
pagination.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerComponent = (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 5,
|
||||||
|
backgroundColor: MainColor.soft_darkblue,
|
||||||
|
borderRadius: 50,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonCustom
|
||||||
|
backgroundColor={
|
||||||
|
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
|
||||||
|
}
|
||||||
|
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
|
||||||
|
style={{ width: "49%" }}
|
||||||
|
onPress={() => handlePress("all")}
|
||||||
|
>
|
||||||
|
Semua Riwayat
|
||||||
|
</ButtonCustom>
|
||||||
|
<Spacing width={"2%"} />
|
||||||
|
<ButtonCustom
|
||||||
|
backgroundColor={
|
||||||
|
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
|
||||||
|
}
|
||||||
|
textColor={
|
||||||
|
activeCategory === "main" ? MainColor.black : MainColor.white
|
||||||
|
}
|
||||||
|
style={{ width: "49%" }}
|
||||||
|
onPress={() => handlePress("main")}
|
||||||
|
>
|
||||||
|
Riwayat Saya
|
||||||
|
</ButtonCustom>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderEventItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
screens/Event/ScreenListOfParticipants.tsx
Normal file
121
screens/Event/ScreenListOfParticipants.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
AvatarUsernameAndOtherComponent,
|
||||||
|
BadgeCustom,
|
||||||
|
BaseBox
|
||||||
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import {
|
||||||
|
apiEventGetOne,
|
||||||
|
apiEventListOfParticipants,
|
||||||
|
} from "@/service/api-client/api-event";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export default function Event_ScreenListOfParticipants() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
return await apiEventListOfParticipants({
|
||||||
|
id: id as string,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [id],
|
||||||
|
onError: (error) =>
|
||||||
|
console.error("[ERROR] Fetch event participants:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onLoadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch event data separately (not part of pagination)
|
||||||
|
// useFocusEffect(() => {
|
||||||
|
// onLoadData();
|
||||||
|
// pagination.onRefresh();
|
||||||
|
// });
|
||||||
|
|
||||||
|
const onLoadData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiEventGetOne({ id: id as string });
|
||||||
|
if (response.success) {
|
||||||
|
const date = dayjs(response.data.tanggal);
|
||||||
|
setStartDate(date);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada peserta",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item participant
|
||||||
|
const renderParticipantItem = ({ item }: { item: any }) => (
|
||||||
|
<BaseBox key={item.id}>
|
||||||
|
<AvatarUsernameAndOtherComponent
|
||||||
|
avatar={item?.User?.Profile?.imageId}
|
||||||
|
name={item?.User?.username}
|
||||||
|
avatarHref={`/profile/${item?.User?.Profile?.id}`}
|
||||||
|
rightComponent={
|
||||||
|
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
|
||||||
|
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||||
|
</BadgeCustom>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BadgeCustom color="gray">-</BadgeCustom>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderParticipantItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
screens/Event/ScreenStatus.tsx
Normal file
122
screens/Event/ScreenStatus.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
BoxWithHeaderSection,
|
||||||
|
Grid,
|
||||||
|
ScrollableCustom,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
|
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||||
|
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
|
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||||
|
import { apiEventGetByStatus } from "@/service/api-client/api-event";
|
||||||
|
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
export default function Event_ScreenStatus() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||||
|
|
||||||
|
const id = user?.id || "";
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||||
|
status || "publish",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page) => {
|
||||||
|
if (!id) return { data: [] };
|
||||||
|
|
||||||
|
return await apiEventGetByStatus({
|
||||||
|
id: id!,
|
||||||
|
status: activeCategory!,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||||
|
dependencies: [id, activeCategory],
|
||||||
|
onError: (error) => console.error("[ERROR] Fetch event by status:", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate komponen
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
skeletonHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render item event
|
||||||
|
const renderEventItem = ({ item }: { item: any }) => (
|
||||||
|
<BoxWithHeaderSection
|
||||||
|
key={item.id}
|
||||||
|
href={`/event/${item.id}/${activeCategory}/detail-event`}
|
||||||
|
>
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextCustom truncate bold>
|
||||||
|
{item?.title}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||||
|
<TextCustom>
|
||||||
|
{new Date(item?.tanggal).toLocaleDateString()}
|
||||||
|
</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</BoxWithHeaderSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
// Reset pagination saat kategori berubah
|
||||||
|
pagination.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
pagination.onRefresh();
|
||||||
|
}, [activeCategory])
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabsComponent = (
|
||||||
|
<ScrollableCustom
|
||||||
|
data={dummyMasterStatus.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as any}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
|
headerComponent={<View style={{ paddingTop: 8 }}>{tabsComponent}</View>}
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderEventItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={pagination.refreshing}
|
||||||
|
onRefresh={pagination.onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
AvatarComp,
|
AvatarComp,
|
||||||
BackButton,
|
BackButton,
|
||||||
FloatingButton,
|
FloatingButton,
|
||||||
SearchInput
|
SearchInput,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -101,8 +101,9 @@ export default function Forum_ViewBeranda3() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<NewWrapper
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
headerComponent={
|
headerComponent={
|
||||||
<View style={{ paddingTop: 8 }}>
|
<View style={{ paddingTop: 8 }}>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
placeholder="Cari topik diskusi"
|
placeholder="Cari topik diskusi"
|
||||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export default function Investment_ButtonStatusSection({
|
|||||||
status: string;
|
status: string;
|
||||||
buttonPublish?: React.ReactNode;
|
buttonPublish?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const path : any= (status: string) => {
|
||||||
|
return `/investment/(tabs)/portofolio?status=${status}`;
|
||||||
|
};
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const handleBatalkanReview = () => {
|
const handleBatalkanReview = () => {
|
||||||
AlertDefaultSystem({
|
AlertDefaultSystem({
|
||||||
@@ -30,13 +33,13 @@ export default function Investment_ButtonStatusSection({
|
|||||||
id: id as string,
|
id: id as string,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
});
|
});
|
||||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
text1: "Berhasil Batalkan Review",
|
text1: "Berhasil Batalkan Review",
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(path("draft"));
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -65,13 +68,13 @@ export default function Investment_ButtonStatusSection({
|
|||||||
id: id as string,
|
id: id as string,
|
||||||
status: "review",
|
status: "review",
|
||||||
});
|
});
|
||||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
text1: "Berhasil Ajukan Review",
|
text1: "Berhasil Ajukan Review",
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(path("review"));
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -100,13 +103,13 @@ export default function Investment_ButtonStatusSection({
|
|||||||
id: id as string,
|
id: id as string,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
});
|
});
|
||||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
text1: "Berhasil Update Status",
|
text1: "Berhasil Update Status",
|
||||||
});
|
});
|
||||||
router.back();
|
router.replace(path("draft"));
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -135,8 +138,6 @@ export default function Investment_ButtonStatusSection({
|
|||||||
id: id as string,
|
id: id as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[RESPONSE DELETE]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user