Compare commits
14 Commits
loaddata/3
...
fixed-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
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
|
||||
- **Framework**: Expo (v54.0.0) with React Native (0.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
|
||||
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
|
||||
- **Language**: TypeScript
|
||||
- **Architecture**: File-based routing with Expo Router
|
||||
- **State Management**: Context API
|
||||
- **UI Components**: React Native Paper, custom components
|
||||
- **Maps Integration**: Mapbox Maps for React Native
|
||||
- **Push Notifications**: React Native Firebase Messaging
|
||||
- **Build System**: Metro bundler
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
hipmi-mobile/
|
||||
├── app/ # File-based routing structure
|
||||
│ ├── (application)/ # Main application screens
|
||||
│ │ ├── (file)/ # File management screens
|
||||
│ │ ├── (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)
|
||||
├── app/ # Main application screens and routing
|
||||
│ ├── _layout.tsx # Root layout component
|
||||
│ ├── index.tsx # Entry point (Login screen)
|
||||
│ └── ...
|
||||
├── components/ # Reusable UI components
|
||||
├── constants/ # Configuration constants
|
||||
├── context/ # React Context providers
|
||||
├── context/ # State management (AuthContext)
|
||||
├── screens/ # Screen components organized by feature
|
||||
│ ├── Admin/ # Admin panel screens
|
||||
│ ├── Authentication/ # Login, registration flows
|
||||
│ ├── Collaboration/ # Collaboration features
|
||||
│ ├── Event/ # Event management
|
||||
│ ├── Forum/ # Forum functionality
|
||||
│ ├── Home/ # Home screen
|
||||
│ ├── Maps/ # Map integration
|
||||
│ ├── Profile/ # User profile
|
||||
│ └── ...
|
||||
├── assets/ # Images, icons, and static assets
|
||||
├── constants/ # Constants and configuration values
|
||||
├── hooks/ # Custom React hooks
|
||||
├── screens/ # Screen components
|
||||
├── service/ # API services and configurations
|
||||
├── lib/ # Utility libraries
|
||||
├── navigation/ # Navigation configuration
|
||||
├── service/ # API services and business logic
|
||||
├── types/ # TypeScript type definitions
|
||||
├── app.config.js # Expo configuration
|
||||
├── package.json # Dependencies and scripts
|
||||
└── ...
|
||||
└── utils/ # Helper functions
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (with bun >=1.0.0 as specified in package.json)
|
||||
- Expo CLI or bun installed globally
|
||||
- Node.js (with bun as the package manager)
|
||||
- Expo CLI
|
||||
- iOS Simulator or Android Emulator (for native builds)
|
||||
|
||||
### Setup Instructions
|
||||
1. **Install dependencies**:
|
||||
### Setup and Development
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
bun install
|
||||
# or if using npm
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Environment Variables**:
|
||||
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**:
|
||||
2. **Run Development Server**
|
||||
```bash
|
||||
# Using bun (as specified in package.json)
|
||||
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
|
||||
# Android
|
||||
bun run android
|
||||
# iOS
|
||||
bun run ios
|
||||
# Web
|
||||
bun run web
|
||||
bun run lint
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
The application uses environment variables defined in the app.config.js file:
|
||||
- `API_BASE_URL`: Base URL for API endpoints
|
||||
- `BASE_URL`: Base application URL
|
||||
- `DEEP_LINK_URL`: URL for deep linking functionality
|
||||
|
||||
### EAS Build Configuration
|
||||
The project uses Expo Application Services (EAS) for building and deployment:
|
||||
- Development builds: `eas build --profile development`
|
||||
- Preview builds: `eas build --profile preview`
|
||||
- Production builds: `eas build --profile production`
|
||||
The project uses Expo Application Services (EAS) for building and deploying:
|
||||
- Development builds with development client
|
||||
- Preview builds for internal distribution
|
||||
- Production builds for app stores
|
||||
|
||||
## Authentication Flow
|
||||
## Features and Functionality
|
||||
|
||||
The application implements a phone number-based authentication system with OTP verification:
|
||||
|
||||
1. **Login**: User enters phone number → OTP sent via SMS
|
||||
2. **Verification**: User enters OTP code → Validates and creates session
|
||||
3. **Registration**: If user doesn't exist, registration flow is triggered
|
||||
4. **Terms Agreement**: User must accept terms and conditions
|
||||
5. **Session Management**: Tokens stored in AsyncStorage
|
||||
|
||||
### Key Authentication Functions
|
||||
- `loginWithNomor()`: Initiates OTP sending
|
||||
- `validateOtp()`: Verifies OTP and creates session
|
||||
- `registerUser()`: Registers new users
|
||||
- `logout()`: Clears session and removes tokens
|
||||
- `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
|
||||
The application appears to include several key modules:
|
||||
- **Authentication**: Login, registration, and verification flows
|
||||
- **Admin Panel**: Administrative functions
|
||||
- **Collaboration**: Tools for member collaboration
|
||||
- **Events**: Event management and calendar
|
||||
- **Forum**: Discussion forums
|
||||
- **Maps**: Location-based services with Mapbox integration
|
||||
- **Donations**: Donation functionality
|
||||
- **Job Board**: Employment opportunities
|
||||
- **Investment**: Investment-related features
|
||||
- **Voting**: Voting systems
|
||||
- **Portfolio**: Member portfolio showcase
|
||||
- **Notifications**: Push notifications via Firebase
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Naming Conventions
|
||||
- Components: PascalCase (e.g., `UserProfile.tsx`)
|
||||
- Functions: camelCase (e.g., `getUserData()`)
|
||||
- Constants: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)
|
||||
- Files: kebab-case or camelCase for utility files
|
||||
|
||||
### 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
|
||||
### Coding Standards
|
||||
- TypeScript is used throughout the project for type safety
|
||||
- Component-based architecture with reusable components
|
||||
- Context API for state management
|
||||
- File-based routing with Expo Router
|
||||
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||
|
||||
### Testing
|
||||
- Linting: `bun run lint` (uses ESLint with Expo config)
|
||||
- No specific test framework mentioned in package.json
|
||||
- Linting is configured with ESLint
|
||||
- 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:
|
||||
- Environment variables loaded via dotenv
|
||||
- Expo's extra configuration in `app.config.js`
|
||||
- Platform-specific configurations for iOS and Android
|
||||
## Key Dependencies
|
||||
|
||||
### Supported Platforms
|
||||
- iOS (with tablet support)
|
||||
- Android (with adaptive icons)
|
||||
- Web (static output)
|
||||
### Core Dependencies
|
||||
- `@react-navigation/*`: Navigation solution for React Native
|
||||
- `@react-native-firebase/*`: Firebase integration for React Native
|
||||
- `@rnmapbox/maps`: Mapbox integration for React Native
|
||||
- `expo-router`: File-based routing for Expo applications
|
||||
- `react-native-paper`: Material Design components for React Native
|
||||
- `react-native-toast-message`: Toast notifications
|
||||
- `react-native-otp-entry`: OTP input components
|
||||
- `react-native-qrcode-svg`: QR code generation
|
||||
|
||||
## Third-party Integrations
|
||||
### Development Dependencies
|
||||
- `@types/*`: TypeScript type definitions
|
||||
- `eslint-config-expo`: Expo-specific ESLint configuration
|
||||
- `typescript`: Type checking
|
||||
|
||||
- **Firebase**: Authentication, messaging, and analytics
|
||||
- **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
|
||||
## Platform Support
|
||||
|
||||
## 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
|
||||
- `eas.json`: EAS build profiles and submission configuration
|
||||
- `tsconfig.json`: TypeScript compiler options
|
||||
- `package.json`: Dependencies, scripts, and project metadata
|
||||
- `metro.config.js`: Metro bundler configuration
|
||||
## Special Configurations
|
||||
|
||||
### iOS Configuration
|
||||
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||
- Supports tablets
|
||||
- Google Services integration
|
||||
- Location permission handling
|
||||
- Associated domains for deep linking
|
||||
|
||||
### Android Configuration
|
||||
- Package name: `com.bip.hipmimobileapp`
|
||||
- Adaptive icons
|
||||
- Edge-to-edge display enabled
|
||||
- Intent filters for HTTPS deep linking
|
||||
- Google Services integration
|
||||
|
||||
### Maps Integration
|
||||
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||
|
||||
### Push Notifications
|
||||
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||
@@ -1,56 +1,9 @@
|
||||
import {
|
||||
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";
|
||||
import Donation_ScreenBeranda from "@/screens/Donation/ScreenBeranda";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Donation_ScreenBeranda />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,148 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
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";
|
||||
import Donation_ScreenMyDonation from "@/screens/Donation/ScreenMyDonation";
|
||||
|
||||
export default function DonationMyDonation() {
|
||||
const { user } = useAuth();
|
||||
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>
|
||||
);
|
||||
return <Donation_ScreenMyDonation />;
|
||||
}
|
||||
|
||||
@@ -1,82 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Donation_ScreenStatus from "@/screens/Donation/ScreenStatus";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function DonationStatus() {
|
||||
const { user } = useAuth();
|
||||
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 (
|
||||
<ViewWrapper hideFooter headerComponent={scrollComponent}>
|
||||
{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>
|
||||
<Donation_ScreenStatus initialStatus={status || "publish"} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
@@ -31,7 +32,7 @@ export default function DonationEditNews() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [news])
|
||||
}, [news]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -104,7 +105,21 @@ export default function DonationEditNews() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<ViewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data?.title || !data?.deskripsi}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||
<LandscapeFrameUploaded
|
||||
@@ -148,15 +163,6 @@ export default function DonationEditNews() {
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
disabled={!data?.title || !data?.deskripsi}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
@@ -53,7 +55,7 @@ export default function DonationAddNews() {
|
||||
text1: "Gagal menambah berita",
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
@@ -70,7 +72,21 @@ export default function DonationAddNews() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data.title || !data.deskripsi}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
|
||||
<LandscapeFrameUploaded image={image?.uri} />
|
||||
@@ -116,17 +132,7 @@ export default function DonationAddNews() {
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
disabled={!data.title || !data.deskripsi}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import Donation_ScreenListOfNews from "@/screens/Donation/ScreenListOfNews";
|
||||
|
||||
export default function DonationRecapOfNews() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [list, setList] = useState<any[] | null>(null);
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return <Donation_ScreenListOfNews donationId={id as string} />;
|
||||
}
|
||||
|
||||
@@ -1,112 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import Donation_ScreenRecapOfNews from "@/screens/Donation/ScreenRecapOfNews";
|
||||
|
||||
export default function DonationRecapOfNews() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [list, setList] = useState<any[] | null>(null);
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return <Donation_ScreenRecapOfNews donationId={id as string} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
Grid,
|
||||
@@ -35,7 +36,7 @@ export default function DonationInvoice() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [invoiceId])
|
||||
}, [invoiceId]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -100,7 +101,22 @@ export default function DonationInvoice() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!image}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerUpdateInvoice();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
<InformationBox
|
||||
text={`Mohon transfer donasi anda ke rekening dibawah`}
|
||||
@@ -204,16 +220,6 @@ export default function DonationInvoice() {
|
||||
</ButtonCenteredOnly>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<ButtonCustom
|
||||
disabled={!image}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerUpdateInvoice();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
DotButton,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconEdit, IconNews } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
|
||||
@@ -26,18 +27,19 @@ import {
|
||||
} from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function DonasiDetailStatus() {
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
||||
|
||||
const [data, setData] = useState<any>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -94,31 +107,50 @@ export default function DonasiDetailStatus() {
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<Donation_ComponentBoxDetailData
|
||||
sisaHari={value.sisa}
|
||||
reminder={value.reminder}
|
||||
data={data}
|
||||
bottomSection={
|
||||
status === "publish" && (
|
||||
<Donation_ProgressSection
|
||||
<NewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!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}
|
||||
progres={Number(data?.progres) || 0}
|
||||
status={status as string}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Donation_ComponentStoryFunrising
|
||||
id={id as string}
|
||||
dataStory={data?.CeritaDonasi}
|
||||
/>
|
||||
<Spacing />
|
||||
<Donation_ButtonStatusSection
|
||||
id={id as string}
|
||||
status={status as string}
|
||||
/>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
</>
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import API_IMAGE from "@/constants/api-storage";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import {
|
||||
@@ -60,7 +63,7 @@ export default function DonationEdit() {
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
onLoadList();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -79,7 +82,6 @@ export default function DonationEdit() {
|
||||
imageId: response.data.imageId,
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
@@ -182,10 +184,24 @@ export default function DonationEdit() {
|
||||
};
|
||||
|
||||
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." />
|
||||
{!data || loadList ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -260,17 +276,9 @@ export default function DonationEdit() {
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,124 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import Donation_ScreenFundDisbursement from "@/screens/Donation/ScreenFundDisbursement";
|
||||
|
||||
export default function DonationFundDisbursement() {
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
const [data, setData] = useState({
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return <Donation_ScreenFundDisbursement donationId={id as string} />;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import {
|
||||
DotButton,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconNews } from "@/components/_Icon";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
|
||||
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
|
||||
@@ -34,7 +36,7 @@ export default function DonasiDetailBeranda() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -75,10 +77,10 @@ export default function DonasiDetailBeranda() {
|
||||
<>
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={value?.reminder}
|
||||
disabled={value?.reminder || !data}
|
||||
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
|
||||
>
|
||||
{value?.reminder ? "Waktu berakhir" : "Donasi"}
|
||||
{!data ? "Loading..." : value?.reminder ? "Waktu berakhir" : "Donasi"}
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
@@ -96,21 +98,30 @@ export default function DonasiDetailBeranda() {
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper footerComponent={buttonSection}>
|
||||
<StackCustom>
|
||||
<Donation_ComponentBoxDetailData
|
||||
sisaHari={value.sisa}
|
||||
reminder={value.reminder}
|
||||
data={data}
|
||||
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
|
||||
/>
|
||||
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
||||
<Donation_ComponentStoryFunrising
|
||||
id={id as string}
|
||||
dataStory={data?.CeritaDonasi}
|
||||
/>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
<NewWrapper footerComponent={buttonSection}>
|
||||
{!data ? (
|
||||
<CustomSkeleton height={400} />
|
||||
) : (
|
||||
<StackCustom>
|
||||
<Donation_ComponentBoxDetailData
|
||||
sisaHari={value.sisa}
|
||||
reminder={value.reminder}
|
||||
data={data}
|
||||
bottomSection={
|
||||
<Donation_ProgressSection
|
||||
id={id as string}
|
||||
progres={Number(data?.progres) || 0}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
|
||||
<Donation_ComponentStoryFunrising
|
||||
id={id as string}
|
||||
dataStory={data?.CeritaDonasi}
|
||||
/>
|
||||
</StackCustom>
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
|
||||
@@ -1,94 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import Donation_ScreenListOfDonatur from "@/screens/Donation/ScreenListOfDonatur";
|
||||
|
||||
export default function Donation_ListOfDonatur() {
|
||||
export default function DonationListOfDonatur() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return <Donation_ScreenListOfDonatur donationId={id as string} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
@@ -112,7 +113,23 @@ export default function DonationCreateStory() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
footerComponent={
|
||||
<>
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<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." />
|
||||
<TextAreaCustom
|
||||
@@ -166,18 +183,8 @@ export default function DonationCreateStory() {
|
||||
value={data.rekening}
|
||||
onChangeText={(value) => setData({ ...data, rekening: value })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { apiDonationCreate } from "@/service/api-client/api-donation";
|
||||
import { apiMasterDonation } from "@/service/api-client/api-master";
|
||||
@@ -43,7 +44,7 @@ export default function DonationCreate() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadList();
|
||||
}, [])
|
||||
}, []),
|
||||
);
|
||||
|
||||
const onLoadList = async () => {
|
||||
@@ -125,7 +126,24 @@ export default function DonationCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
footerComponent={
|
||||
<>
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
||||
}}
|
||||
>
|
||||
Selanjutnya
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<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 })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
|
||||
}}
|
||||
>
|
||||
Selanjutnya
|
||||
</ButtonCustom>
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
|
||||
|
||||
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 (
|
||||
<ViewWrapper hideFooter>
|
||||
{isLoadList ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenContribution />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,104 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
|
||||
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";
|
||||
import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={headerComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,9 @@
|
||||
import { LoaderCustom, TextCustom } from "@/components";
|
||||
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";
|
||||
import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Event_ScreenBeranda />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={tabsComponent}>
|
||||
{loadingGetData ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenStatus />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import {
|
||||
apiEventGetOne,
|
||||
@@ -48,14 +51,14 @@ export default function EventEdit() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiEventGetOne({ id: id as string });
|
||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
setSelectedDate(new Date(response.data.tanggal));
|
||||
@@ -100,6 +103,15 @@ export default function EventEdit() {
|
||||
const startDate = new Date(selectedDate 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) {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -146,7 +158,7 @@ export default function EventEdit() {
|
||||
|
||||
const validateDateRange = (
|
||||
selectedDate: string | Date,
|
||||
selectedEndDate: string | Date
|
||||
selectedEndDate: string | Date,
|
||||
): { isValid: boolean; error?: string } => {
|
||||
const startDate = new Date(selectedDate);
|
||||
const endDate = new Date(selectedEndDate);
|
||||
@@ -174,9 +186,19 @@ export default function EventEdit() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
/>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -186,26 +208,15 @@ export default function EventEdit() {
|
||||
value={data?.title}
|
||||
onChangeText={(value) => setData({ ...data, title: value })}
|
||||
/>
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe 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"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
minimumDate={new Date(Date.now())}
|
||||
label="Tanggal & Waktu Mulai"
|
||||
@@ -233,7 +244,7 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
@@ -242,31 +253,37 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe 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 });
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
|
||||
|
||||
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 (
|
||||
<ViewWrapper>
|
||||
{loadtData && !listData ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenListOfParticipants />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -100,7 +101,6 @@ export default function EventCreate() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const buttonSubmit = (
|
||||
<ButtonCustom
|
||||
@@ -112,7 +112,9 @@ export default function EventCreate() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
placeholder="Masukkan nama event"
|
||||
@@ -121,24 +123,15 @@ export default function EventCreate() {
|
||||
onChangeText={(value: any) => setData({ ...data, title: value })}
|
||||
/>
|
||||
|
||||
<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"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
@@ -168,22 +161,28 @@ export default function EventCreate() {
|
||||
</TextCustom>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
{buttonSubmit}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { StackCustom, ViewWrapper } from "@/components";
|
||||
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
@@ -29,14 +29,14 @@ export default function Application() {
|
||||
checkVersion();
|
||||
userData(token as string);
|
||||
syncUnreadCount();
|
||||
}, [user?.id, token])
|
||||
}, [user?.id, token]),
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
const response = await apiUser(user?.id as string);
|
||||
console.log(
|
||||
"[Profile ID]>>",
|
||||
JSON.stringify(response?.data?.Profile?.id, null, 2)
|
||||
JSON.stringify(response?.data?.Profile?.id, null, 2),
|
||||
);
|
||||
|
||||
setData(response.data);
|
||||
@@ -61,14 +61,31 @@ export default function Application() {
|
||||
|
||||
if (data && data?.active === false) {
|
||||
console.log("User is not active");
|
||||
return <Redirect href={`/waiting-room`} />;
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/waiting-room`} />
|
||||
</BasicWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (data && data?.Profile === 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 (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -89,7 +106,12 @@ export default function Application() {
|
||||
/>
|
||||
<ViewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
footerComponent={
|
||||
<TabSection
|
||||
|
||||
@@ -1,56 +1,9 @@
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenBursa from "@/screens/Invesment/ScreenBursa";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Investment_ScreenBursa />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenMyHolding from "@/screens/Invesment/ScreenMyHolding";
|
||||
|
||||
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 (
|
||||
<ViewWrapper hideFooter>
|
||||
{loadingList ? (
|
||||
<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>
|
||||
<>
|
||||
<Investment_ScreenMyHolding />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,82 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenPortofolio from "@/screens/Invesment/ScreenPortofolio";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
||||
{loadingList ? (
|
||||
<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>
|
||||
<>
|
||||
<Investment_ScreenPortofolio />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,124 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
|
||||
|
||||
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 (
|
||||
<ViewWrapper hideFooter>
|
||||
{loadList ? (
|
||||
<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>
|
||||
<>
|
||||
<Investment_ScreenTransaction />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
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";
|
||||
import Investment_ScreenListOfDocument from "@/screens/Invesment/Document/ScreenListDocument";
|
||||
|
||||
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 (
|
||||
<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}
|
||||
href={`/(file)/${item.fileId}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Investment_ScreenListOfDocument />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,213 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenRecapOfDocument from "@/screens/Invesment/Document/ScreenRecapOfDocument";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack.Screen
|
||||
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>
|
||||
<Investment_ScreenRecapOfDocument />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,100 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenListOfNews from "@/screens/Invesment/News/ScreenListOfNews";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function InvestmentListOfNews() {
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Investment_ScreenListOfNews investmentId={id as string} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenRecapOfNews from "@/screens/Invesment/News/ScreenRecapOfNews";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function InvestmentRecapOfNews() {
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Investment_ScreenRecapOfNews investmentId={id as string} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,230 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
Grid,
|
||||
InformationBox,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CopyButton from "@/components/Button/CoyButton";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import {
|
||||
apiInvestmentGetInvoice,
|
||||
apiInvestmentUpdateInvoice,
|
||||
} from "@/service/api-client/api-investment";
|
||||
import { uploadFileService } from "@/service/upload-service";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import Investment_ScreenInvoice from "@/screens/Invesment/ScreenInvoice";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<StackCustom>
|
||||
<InformationBox text="Mohon transfer ke rekening dibawah" />
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
<TextCustom>Bank</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Spacing height={10} />
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
<TextCustom>Nama Akun</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||
<Grid.Col
|
||||
span={8}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TextCustom size="xlarge" bold color="yellow">
|
||||
{data?.MasterBank?.norek}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CopyButton textToCopy={data?.MasterBank?.norek} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom>Jumlah Transaksi</TextCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
|
||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||
<Grid.Col
|
||||
span={8}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TextCustom size="xlarge" bold color="yellow">
|
||||
Rp. {formatCurrencyDisplay(data?.nominal)}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CopyButton textToCopy={data?.nominal} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<TextCustom align="center">
|
||||
Upload bukti transfer anda.
|
||||
</TextCustom>
|
||||
{image ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 10,
|
||||
paddingInline: 20,
|
||||
}}
|
||||
>
|
||||
<TextCustom bold align="center" truncate>
|
||||
{image?.name}
|
||||
</TextCustom>
|
||||
</View>
|
||||
) : (
|
||||
<TextCustom align="center">
|
||||
Tidak ada gambar yang diunggah
|
||||
</TextCustom>
|
||||
)}
|
||||
<ButtonCenteredOnly
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file: any) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
icon="upload"
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!image || isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
>
|
||||
Saya Sudah Transfer
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
<Investment_ScreenInvoice />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export default function InvestmentSelectBank() {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
console.log("[RESPONSE >>]", response);
|
||||
const invoiceId = response.data.id;
|
||||
|
||||
const delStorage = await AsyncStorage.removeItem(
|
||||
|
||||
@@ -73,7 +73,6 @@ export default function InvestmentDetailStatus() {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||
<LandscapeFrameUploaded
|
||||
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
|
||||
/>
|
||||
|
||||
<TextInputCustom
|
||||
disabled
|
||||
iconLeft="Rp."
|
||||
// disabled
|
||||
required
|
||||
placeholder="0"
|
||||
label="Total Lembar"
|
||||
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ export default function InvestmentDetail() {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
|
||||
@@ -1,67 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Investment_ScreenInvestor from "@/screens/Invesment/ScreenInvestor";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function InvestmentInvestor() {
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Investment_ScreenInvestor investmentId={id as string} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
|
||||
|
||||
// const [coba, setCoba] = useState("");
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handleSubmit()}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||
<LandscapeFrameUploaded image={image as string} />
|
||||
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
|
||||
|
||||
<StackCustom gap={0}>
|
||||
<TextInputCustom
|
||||
disabled
|
||||
iconLeft="Rp."
|
||||
// disabled
|
||||
editable={false}
|
||||
required
|
||||
placeholder="0"
|
||||
label="Total Lembar"
|
||||
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
|
||||
/>
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handleSubmit()}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
{/* <Spacing height={50} /> */}
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
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";
|
||||
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
|
||||
|
||||
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 (
|
||||
<ViewWrapper hideFooter>
|
||||
{isLoadData ? (
|
||||
<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>
|
||||
<>
|
||||
<Job_ScreenArchive2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,10 @@
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
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";
|
||||
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
|
||||
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Job_ScreenBeranda2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
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";
|
||||
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
|
||||
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<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>
|
||||
{/* <Job_MainViewStatus /> */}
|
||||
<Job_MainViewStatus2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -99,16 +100,17 @@ export default function JobCreate() {
|
||||
const buttonSubmit = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
<Spacing />
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
<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." />
|
||||
|
||||
@@ -160,9 +162,7 @@ export default function JobCreate() {
|
||||
value={data.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
{buttonSubmit()}
|
||||
</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() {
|
||||
return (
|
||||
<>
|
||||
<ScreenNotification />
|
||||
<ScreenNotification_V2 />
|
||||
{/* <ScreenNotification_V1 /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export default function ProfileDetailBlocked() {
|
||||
|
||||
const fetchData = async () => {
|
||||
const response = await apiGetBlockedById({ id: String(id) });
|
||||
// console.log("[RESPONSE >>]", JSON.stringify(response, null, 2));
|
||||
setData(response.data);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,59 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
|
||||
|
||||
export default function VotingContribution() {
|
||||
const { user } = useAuth();
|
||||
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>
|
||||
);
|
||||
return <Voting_ScreenContribution />;
|
||||
}
|
||||
|
||||
@@ -1,77 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
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";
|
||||
import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Voting_ScreenHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
|
||||
|
||||
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 (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
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>
|
||||
<>
|
||||
<Voting_ScreenBeranda />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,106 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
||||
{loadingGetData ? (
|
||||
<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>
|
||||
<>
|
||||
<Voting_ScreenStatus />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetOne({ id: id as string });
|
||||
|
||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
@@ -34,7 +35,7 @@ interface IEditData {
|
||||
Voting_DaftarNamaVote?: [
|
||||
{
|
||||
value?: string;
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ export default function VotingEdit() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -188,9 +189,9 @@ export default function VotingEdit() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -210,7 +211,7 @@ export default function VotingEdit() {
|
||||
onChangeText={(text) => setData({ ...data, deskripsi: text })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
|
||||
<DateTimePickerCustom
|
||||
minimumDate={new Date(Date.now())}
|
||||
@@ -255,7 +256,7 @@ export default function VotingEdit() {
|
||||
}
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
|
||||
</StackCustom>
|
||||
|
||||
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
|
||||
@@ -270,7 +271,7 @@ export default function VotingEdit() {
|
||||
...(data as any),
|
||||
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
|
||||
(item: any, i: any) =>
|
||||
i === index ? { ...item, value } : item
|
||||
i === index ? { ...item, value } : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -327,6 +328,6 @@ export default function VotingEdit() {
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
|
||||
|
||||
export default function Voting_ListOfContributor() {
|
||||
const { id } = useLocalSearchParams();
|
||||
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>
|
||||
);
|
||||
export default function VotingListOfContributor() {
|
||||
return <Voting_ScreenListOfContributor />;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -79,7 +80,7 @@ export default function VotingCreate() {
|
||||
type: "success",
|
||||
text1: "Data berhasil disimpan",
|
||||
});
|
||||
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
|
||||
router.replace("/voting/(tabs)/status?status=review");
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -106,7 +107,7 @@ export default function VotingCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
label="Judul Voting"
|
||||
@@ -142,7 +143,6 @@ export default function VotingCreate() {
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{listVote.map((item, index) => (
|
||||
<TextInputCustom
|
||||
key={index}
|
||||
@@ -153,8 +153,8 @@ export default function VotingCreate() {
|
||||
onChangeText={(value: any) =>
|
||||
setListVote(
|
||||
listVote.map((item, i) =>
|
||||
i === index ? { ...item, value } : item
|
||||
)
|
||||
i === index ? { ...item, value } : item,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -198,6 +198,6 @@ export default function VotingCreate() {
|
||||
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/components";
|
||||
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
||||
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
||||
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
ICON_SIZE_MEDIUM,
|
||||
@@ -20,6 +21,10 @@ import {
|
||||
adminListMenu,
|
||||
superAdminListMenu,
|
||||
} from "@/screens/Admin/listPageAdmin";
|
||||
import {
|
||||
adminListMenu_V2,
|
||||
superAdminListMenu_V2,
|
||||
} from "@/screens/Admin/listPageAdmin_V2";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack } from "expo-router";
|
||||
@@ -140,13 +145,22 @@ export default function AdminLayout() {
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
|
||||
<NavbarMenu
|
||||
{/* <NavbarMenu
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu
|
||||
: superAdminListMenu
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
|
||||
<NavbarMenu_V2
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/>
|
||||
</StackCustom>
|
||||
</DrawerAdmin>
|
||||
@@ -198,7 +212,7 @@ export default function AdminLayout() {
|
||||
// size={ICON_SIZE_SMALL}
|
||||
// color={MainColor.white}
|
||||
// />
|
||||
<AdminNotificationBell/>
|
||||
<AdminNotificationBell />
|
||||
),
|
||||
path: "/admin/notification",
|
||||
},
|
||||
|
||||
@@ -1,213 +1,10 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
|
||||
import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<Admin_ScreenNotification2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { Stack } from "expo-router";
|
||||
import { router, Stack } from "expo-router";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton /> }}
|
||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<StackCustom
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// DateTimeInput.tsx
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
@@ -7,7 +6,14 @@ import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from "@react-native-community/datetimepicker";
|
||||
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 TextCustom from "../Text/TextCustom";
|
||||
|
||||
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
const [selectedDate, setSelectedDate] = 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
|
||||
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
|
||||
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
time.getHours(),
|
||||
time.getMinutes(),
|
||||
time.getSeconds(),
|
||||
time.getMilliseconds()
|
||||
time.getMilliseconds(),
|
||||
);
|
||||
return combined;
|
||||
}, []);
|
||||
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
};
|
||||
|
||||
const toggleDatePicker = () => {
|
||||
Keyboard.dismiss();
|
||||
setShowDate(!showDate);
|
||||
};
|
||||
|
||||
const toggleTimePicker = () => {
|
||||
Keyboard.dismiss();
|
||||
setShowTime(!showTime);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
// DateTimeInput.tsx
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from "@react-native-community/datetimepicker";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import { Button, StyleProp, Text, View, ViewStyle } from "react-native";
|
||||
import React, { useState, useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
StyleProp,
|
||||
Text,
|
||||
View,
|
||||
ViewStyle,
|
||||
Keyboard,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
} from "react-native";
|
||||
import ClickableCustom from "../Clickable/ClickableCustom";
|
||||
import TextCustom from "../Text/TextCustom";
|
||||
|
||||
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
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) => {
|
||||
if (event.type === "set" && date !== undefined) {
|
||||
setSelectedDate(date);
|
||||
onChange(date as any);
|
||||
// Hanya perbarui state sementara, bukan state utama
|
||||
setTempSelectedDate(date);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePress = () => {
|
||||
// Sembunyikan keyboard sebelum menampilkan date picker
|
||||
Keyboard.dismiss();
|
||||
// Set state sementara ke nilai saat ini
|
||||
setTempSelectedDate(selectedDate);
|
||||
setShow(!show);
|
||||
};
|
||||
|
||||
// Fungsi untuk menangani klik di luar area picker
|
||||
const handleOutsidePress = () => {
|
||||
if (show) {
|
||||
setShow(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClickableCustom
|
||||
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
||||
))}
|
||||
</ClickableCustom>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<View
|
||||
<Modal
|
||||
animationType="fade"
|
||||
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={{
|
||||
position: "absolute",
|
||||
zIndex: 15,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
// top: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 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,
|
||||
}}
|
||||
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
|
||||
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
|
||||
>
|
||||
{/* <View style={{ alignItems: "flex-start" }}>
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={20}
|
||||
color="black"
|
||||
onPress={() => {
|
||||
setShow(false);
|
||||
setSelectedDate(undefined);
|
||||
{/* <View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<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> */}
|
||||
|
||||
<DateTimePicker
|
||||
value={selectedDate || new Date()}
|
||||
value={tempSelectedDate || new Date()}
|
||||
mode={"datetime"}
|
||||
display="spinner"
|
||||
display="inline"
|
||||
onChange={handleConfirm}
|
||||
minimumDate={minimumDate}
|
||||
maximumDate={maximumDate}
|
||||
themeVariant="light"
|
||||
/>
|
||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||||
<ClickableCustom
|
||||
<View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setShow(false)
|
||||
setSelectedDate(undefined)
|
||||
setShow(false);
|
||||
// Kembalikan ke nilai sebelumnya jika batal
|
||||
setTempSelectedDate(selectedDate);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
backgroundColor: MainColor.placeholder,
|
||||
marginTop: 10,
|
||||
width: "48%",
|
||||
}}
|
||||
>
|
||||
<TextCustom color="black">Batal</TextCustom>
|
||||
</ClickableCustom>
|
||||
</TouchableOpacity>
|
||||
|
||||
<ClickableCustom
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setShow(false)
|
||||
onChange(selectedDate as any)
|
||||
// Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
|
||||
setSelectedDate(tempSelectedDate);
|
||||
onChange(tempSelectedDate as any);
|
||||
setShow(false);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
backgroundColor: MainColor.darkblue,
|
||||
marginTop: 10,
|
||||
width: "48%",
|
||||
}}
|
||||
>
|
||||
<TextCustom>OK</TextCustom>
|
||||
</ClickableCustom>
|
||||
</TouchableOpacity>
|
||||
</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",
|
||||
},
|
||||
});
|
||||
@@ -37,6 +37,90 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||
|
||||
// Fungsi untuk mengecek apakah path cocok dengan item menu
|
||||
// Ini akan mengecek kecocokan eksak atau pola path
|
||||
const isActivePath = (itemPath: string | undefined): boolean => {
|
||||
if (!itemPath || !normalizedPathname) return false;
|
||||
|
||||
// Cocokan eksak
|
||||
if (normalizePath(itemPath) === normalizedPathname) return true;
|
||||
|
||||
// Cocokan pola path seperti /user-access/[id]/index dengan /user-access/index
|
||||
// atau /donation/[id]/detail dengan /donation/index
|
||||
const normalizedItemPath = normalizePath(itemPath);
|
||||
|
||||
// Jika path item adalah bagian dari path saat ini (prefix match)
|
||||
if (normalizedPathname.startsWith(normalizedItemPath + '/')) return true;
|
||||
|
||||
// Jika path saat ini adalah bagian dari path item (misalnya /user-access/detail cocok dengan /user-access)
|
||||
if (normalizedItemPath.startsWith(normalizedPathname + '/')) return true;
|
||||
|
||||
// Jika path item adalah bagian dari path saat ini tanpa id (misalnya /user-access/[id]/index cocok dengan /user-access/index)
|
||||
const itemParts = normalizedItemPath.split('/');
|
||||
const currentParts = normalizedPathname.split('/');
|
||||
|
||||
// Jika panjangnya sama dan hanya berbeda di bagian dinamis [id]
|
||||
if (itemParts.length === currentParts.length) {
|
||||
let match = true;
|
||||
for (let i = 0; i < itemParts.length; i++) {
|
||||
// Jika bagian path item adalah placeholder [id], abaikan
|
||||
if (itemParts[i].startsWith('[') && itemParts[i].endsWith(']')) continue;
|
||||
|
||||
// Jika bagian path saat ini adalah ID (angka), abaikan
|
||||
if (/^\d+$/.test(currentParts[i])) continue;
|
||||
|
||||
// Jika tidak cocok dan bukan placeholder atau ID, maka tidak cocok
|
||||
if (itemParts[i] !== currentParts[i]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
|
||||
// Tambahkan logika khusus untuk menangani file index.tsx sebagai halaman dashboard
|
||||
// Jika path saat ini adalah versi index dari path item (misalnya /admin/event/index cocok dengan /admin/event)
|
||||
if (normalizedPathname === normalizedItemPath + '/index') return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Fungsi untuk menentukan item mana yang paling spesifik aktif
|
||||
// Ini akan memastikan hanya satu item yang aktif pada satu waktu
|
||||
const findMostSpecificActiveItem = (): { parentLabel?: string; subItemLink?: string } | null => {
|
||||
// Cek setiap item menu
|
||||
for (const item of items) {
|
||||
// Jika item memiliki sub-menu
|
||||
if (item.links && item.links.length > 0) {
|
||||
// Urutkan sub-menu berdasarkan panjang path (terpanjang dulu untuk prioritas lebih spesifik)
|
||||
const sortedSubItems = [...item.links].sort((a, b) => {
|
||||
if (a.link && b.link) {
|
||||
return b.link.length - a.link.length; // Urutan menurun (terpanjang dulu)
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Cek setiap sub-menu dalam urutan yang telah diurutkan
|
||||
for (const subItem of sortedSubItems) {
|
||||
if (isActivePath(subItem.link)) {
|
||||
return { parentLabel: item.label, subItemLink: subItem.link };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada sub-menu yang cocok, cek item utama
|
||||
if (isActivePath(item.link)) {
|
||||
return { parentLabel: item.label };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
// Hitung item aktif terlebih dahulu
|
||||
const mostSpecificActive = findMostSpecificActiveItem();
|
||||
|
||||
// Set activeLink saat pathname berubah
|
||||
useEffect(() => {
|
||||
if (normalizedPathname) {
|
||||
@@ -44,6 +128,15 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
}
|
||||
}, [normalizedPathname]);
|
||||
|
||||
// Fungsi untuk menentukan apakah dropdown harus tetap terbuka
|
||||
// Dropdown tetap terbuka jika salah satu dari sub-menu cocok dengan path saat ini
|
||||
const shouldDropdownBeOpen = (item: NavbarItem): boolean => {
|
||||
if (!normalizedPathname || !item.links || item.links.length === 0) return false;
|
||||
|
||||
// Cek apakah salah satu sub-menu cocok dengan path saat ini
|
||||
return item.links.some(subItem => isActivePath(subItem.link));
|
||||
};
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleOpen = (label: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
@@ -56,7 +149,7 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
style={{
|
||||
// flex: 1,
|
||||
// backgroundColor: MainColor.black,
|
||||
marginBottom: 20,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
@@ -72,8 +165,21 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
onClose={onClose}
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
isOpen={openKeys.includes(item.label) || shouldDropdownBeOpen(item)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
isActivePath={isActivePath}
|
||||
isMostSpecificActive={(menuItem) => {
|
||||
if (!mostSpecificActive) return false;
|
||||
|
||||
// Jika item memiliki sub-menu
|
||||
if (menuItem.links && menuItem.links.length > 0) {
|
||||
// Jika item ini adalah parent dari sub-menu yang aktif, menu utama tidak aktif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Jika tidak ada sub-menu, hanya periksa kecocokan langsung
|
||||
return mostSpecificActive.parentLabel === menuItem.label && !mostSpecificActive.subItemLink;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
@@ -89,6 +195,8 @@ function MenuItem({
|
||||
setActiveLink,
|
||||
isOpen,
|
||||
toggleOpen,
|
||||
isActivePath,
|
||||
isMostSpecificActive,
|
||||
}: {
|
||||
item: NavbarItem;
|
||||
onClose?: () => void;
|
||||
@@ -96,8 +204,10 @@ function MenuItem({
|
||||
setActiveLink: (link: string | null) => void;
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
isActivePath: (itemPath: string | undefined) => boolean;
|
||||
isMostSpecificActive: (item: NavbarItem) => boolean;
|
||||
}) {
|
||||
const isActive = activeLink === item.link;
|
||||
const isActive = isMostSpecificActive(item);
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Animasi saat isOpen berubah
|
||||
@@ -121,7 +231,9 @@ function MenuItem({
|
||||
color={MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.parentText}>{item.label}</Text>
|
||||
<Text style={styles.parentText}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
@@ -147,7 +259,8 @@ function MenuItem({
|
||||
]}
|
||||
>
|
||||
{item.links.map((subItem, index) => {
|
||||
const isSubActive = activeLink === subItem.link;
|
||||
// Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath
|
||||
const isSubActive = isActivePath(subItem.link);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
|
||||
570
components/Drawer/NavbarMenu_V2.tsx
Normal file
570
components/Drawer/NavbarMenu_V2.tsx
Normal file
@@ -0,0 +1,570 @@
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { 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;
|
||||
}[];
|
||||
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 = [
|
||||
// Event
|
||||
"type-create",
|
||||
|
||||
// Other
|
||||
"detail",
|
||||
"edit",
|
||||
"create",
|
||||
"new",
|
||||
"add",
|
||||
"delete",
|
||||
"view",
|
||||
"publish",
|
||||
"review",
|
||||
"reject",
|
||||
"status",
|
||||
"category",
|
||||
"history",
|
||||
"type-of-event",
|
||||
"posting",
|
||||
"report-posting",
|
||||
"report-comment",
|
||||
"group",
|
||||
"dashboard",
|
||||
"sticker",
|
||||
"active",
|
||||
"inactive",
|
||||
"pending",
|
||||
"transaction-detail",
|
||||
"transaction",
|
||||
"payment",
|
||||
"disbursement",
|
||||
"list-of-investor",
|
||||
];
|
||||
|
||||
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("list-of-investor") ||
|
||||
currentPath.includes("type-create")
|
||||
) {
|
||||
console.log(
|
||||
"🔍 Pattern Match Check:",
|
||||
JSON.stringify(
|
||||
{
|
||||
currentPath,
|
||||
detailPattern,
|
||||
regex: patternRegex.toString(),
|
||||
isMatch,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 = [
|
||||
// Event
|
||||
"type-create",
|
||||
"detail",
|
||||
"edit",
|
||||
"create",
|
||||
"new",
|
||||
"add",
|
||||
"delete",
|
||||
"view",
|
||||
"publish",
|
||||
"review",
|
||||
"reject",
|
||||
"status",
|
||||
"category",
|
||||
"history",
|
||||
"type-of-event",
|
||||
"posting",
|
||||
"report-posting",
|
||||
"report-comment",
|
||||
"group",
|
||||
"dashboard",
|
||||
"sticker",
|
||||
"active",
|
||||
"inactive",
|
||||
"pending",
|
||||
"transaction-detail",
|
||||
"transaction",
|
||||
"payment",
|
||||
"disbursement",
|
||||
];
|
||||
|
||||
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) {
|
||||
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",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.links.map((subItem, index) => {
|
||||
const isSubActive = isPathActive(
|
||||
subItem.link,
|
||||
subItem.detailPattern,
|
||||
);
|
||||
|
||||
// CRITICAL FIX: Jika submenu ini aktif, cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||
// Jika ada yang lebih panjang dan aktif, maka yang pendek TIDAK AKTIF
|
||||
const hasMoreSpecificMatch = item.links!.some((otherSubItem) => {
|
||||
if (otherSubItem.link === subItem.link) return false; // Skip self
|
||||
|
||||
const otherIsActive = isPathActive(
|
||||
otherSubItem.link,
|
||||
otherSubItem.detailPattern,
|
||||
);
|
||||
const isOtherLonger =
|
||||
otherSubItem.link.length > subItem.link.length;
|
||||
|
||||
// Debug log
|
||||
if (isSubActive && otherIsActive) {
|
||||
console.log(
|
||||
"🔍 CONFLICT DETECTED:",
|
||||
JSON.stringify(
|
||||
{
|
||||
current: subItem.label,
|
||||
currentPath: subItem.link,
|
||||
currentLength: subItem.link.length,
|
||||
other: otherSubItem.label,
|
||||
otherPath: otherSubItem.link,
|
||||
otherLength: otherSubItem.link.length,
|
||||
isOtherLonger,
|
||||
currentURL: currentPath,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Jika submenu lain JUGA aktif DAN lebih panjang (lebih spesifik),
|
||||
// maka submenu yang pendek ini TIDAK boleh aktif
|
||||
return otherIsActive && isOtherLonger;
|
||||
});
|
||||
|
||||
// Final decision: aktif HANYA jika match DAN tidak ada yang lebih spesifik
|
||||
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||
|
||||
// Debug final decision
|
||||
if (isSubActive) {
|
||||
console.log(
|
||||
"✅ Active check:",
|
||||
JSON.stringify(
|
||||
{
|
||||
label: subItem.label,
|
||||
link: subItem.link,
|
||||
isSubActive,
|
||||
hasMoreSpecificMatch,
|
||||
finalIsActive,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
|
||||
|
||||
const fcmToken = await getToken(messagingInstance);
|
||||
if (!fcmToken) {
|
||||
console.warn("Tidak bisa mendapatkan FCM token");
|
||||
console.log("Tidak bisa mendapatkan FCM token");
|
||||
// logout();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ FCM Token:", fcmToken);
|
||||
|
||||
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}
|
||||
keyExtractor={
|
||||
listProps.keyExtractor ||
|
||||
((item) => {
|
||||
((item, index) => {
|
||||
if (item.id == null) {
|
||||
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 GridComponentView from "./_ShareComponent/GridSectionView";
|
||||
import NewWrapper from "./_ShareComponent/NewWrapper";
|
||||
import BasicWrapper from "./_ShareComponent/BasicWrapper";
|
||||
// Progress
|
||||
import ProgressCustom from "./Progress/ProgressCustom";
|
||||
// Loader
|
||||
@@ -121,6 +122,7 @@ export {
|
||||
GridComponentView,
|
||||
Spacing,
|
||||
NewWrapper,
|
||||
BasicWrapper,
|
||||
// Stack
|
||||
StackCustom,
|
||||
TabBarBackground,
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
<!-- Start Penerapan Pagination -->
|
||||
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
|
||||
|
||||
File utama: screens/Notification/ScreenNotification.tsx
|
||||
Fun fecth: apiGetNotificationsById
|
||||
File fetch: service/api-notifications.ts
|
||||
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
|
||||
Folder tujuan: screens/Donation
|
||||
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
|
||||
|
||||
Terapkan pagination pada file "File utama"
|
||||
@@ -10,13 +38,84 @@ 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
|
||||
|
||||
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"
|
||||
|
||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
|
||||
<!-- End Penerapan Pagination -->
|
||||
|
||||
<!-- Additinal prompt -->
|
||||
Masukan kode berikut di prop ListHeaderComponent:
|
||||
<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>
|
||||
|
||||
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
|
||||
|
||||
<!-- Start Penerapan NewWrapper -->
|
||||
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
|
||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
|
||||
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||
<!-- End Penerapan NewWrapper -->
|
||||
|
||||
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
|
||||
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
|
||||
jika tidak maka terapkan sesuai dengan logika yang sudah ada
|
||||
|
||||
Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
|
||||
Ini adalah halaman yang memiliki fungsi pagination , saya membuat data dummy dimana menghasilkan data urut 1-9, saya mencoba memuat halaman setiap page nya 4 saja untuk percobaan.
|
||||
Saat awal muncul komponent box dengan data 9 - 6, kemudian saya hapus data ke 8 . lalu saya coba scroll ke bawah seharusnya angka akan tetap urut 9, 7, 6, 5, 4 ... 1. Tapi dalam case ini setelah 8 di hapus kemudian saya scroll box ke 5 tidak muncul saat di scroll. Apakah anda mengerti maksud saya ?
|
||||
|
||||
<!-- COMMIT & PUSH-->
|
||||
Branch: loaddata/10-feb-26
|
||||
Jalankan perintah ini: git checkout -b "Branch"
|
||||
Setelah itu jalankan perintah ini: git add .
|
||||
Setelah itu jalankan perintah ini: git commit -m "
|
||||
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
|
||||
"
|
||||
Setelah itu jalankan perintah ini: git push origin "Branch"
|
||||
|
||||
|
||||
|
||||
<!-- Start Random Prompt -->
|
||||
|
||||
Saya memiliki case pada file ini: @components/Drawer/NavbarMenu.tsx
|
||||
Pada file ini saya ingin jika saat pindah halaman ( ke detail contoh : /user-access/[id]/index.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya hilang karena tidak mendeteksi halaman tersebut.
|
||||
Apakah anda paham maksud saya ?
|
||||
|
||||
|
||||
Ya, dalam fitur yang anda perbaharui masih terjadi bug. Saya akan berikan case nya secara perlahan
|
||||
Saat klik sebuah menu maka sub menu akan terbuka
|
||||
Saat klik sub menu maka sub menu maka akan menuju ke halaman sesuai path
|
||||
Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terselesaikan. Tapi muncul bug baru jika menu tersebut memiliki sub menu dan jika sub menu tersebut di klik (kecuali dashboard) yang aktif adalah bagian sub menu dashbaord dan sub menu yang kita klik, tapi jika sub menu yang di klik adalah dashboard maka semau sub menu aktif. Apakah anda mengerti maksud dari pernyataan saya ? Jika masih kurang paham saya bisa berikan masukan yang lain
|
||||
|
||||
Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ?
|
||||
|
||||
<!-- End Random Prompt -->
|
||||
|
||||
|
||||
export interface NavbarItem_V2 {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
detailPattern?: string;
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 [isLoadingDelete, setLoadingDelete] = useState(false);
|
||||
const path: any = (status: string) => {
|
||||
return `/donation/(tabs)/status?status=${status}`;
|
||||
};
|
||||
const handleBatalkanReview = async () => {
|
||||
AlertDefaultSystem({
|
||||
title: "Batalkan Review",
|
||||
@@ -43,7 +46,7 @@ export default function Donation_ButtonStatusSection({
|
||||
text1: response.message,
|
||||
});
|
||||
|
||||
router.back();
|
||||
router.push(path("draft"));
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
@@ -80,7 +83,7 @@ export default function Donation_ButtonStatusSection({
|
||||
text1: response.message,
|
||||
});
|
||||
|
||||
router.back();
|
||||
router.replace(path("review"));
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
@@ -117,7 +120,7 @@ export default function Donation_ButtonStatusSection({
|
||||
text1: response.message,
|
||||
});
|
||||
|
||||
router.back();
|
||||
router.replace(path("draft"));
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
|
||||
@@ -12,11 +12,13 @@ import { View } from "react-native";
|
||||
export default function Donation_ComponentBoxDetailData({
|
||||
bottomSection,
|
||||
data,
|
||||
showSisaHari = true,
|
||||
sisaHari,
|
||||
reminder,
|
||||
}: {
|
||||
bottomSection?: React.ReactNode;
|
||||
data: any;
|
||||
showSisaHari?: boolean;
|
||||
sisaHari: number;
|
||||
reminder: boolean;
|
||||
}) {
|
||||
@@ -34,9 +36,9 @@ export default function Donation_ComponentBoxDetailData({
|
||||
<TextCustom bold color="red">
|
||||
Waktu berakhir
|
||||
</TextCustom>
|
||||
) : (
|
||||
) : showSisaHari ? (
|
||||
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<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 {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=draft`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -65,7 +65,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=review`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -99,7 +99,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=draft`);
|
||||
} else {
|
||||
Toast.show({
|
||||
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,
|
||||
BackButton,
|
||||
FloatingButton,
|
||||
SearchInput
|
||||
SearchInput,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -101,8 +101,9 @@ export default function Forum_ViewBeranda3() {
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={
|
||||
<View style={{ paddingTop: 8 }}>
|
||||
<View style={{ paddingTop: 8 }}>
|
||||
<SearchInput
|
||||
placeholder="Cari topik diskusi"
|
||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||
|
||||
@@ -16,6 +16,9 @@ export default function Investment_ButtonStatusSection({
|
||||
status: string;
|
||||
buttonPublish?: React.ReactNode;
|
||||
}) {
|
||||
const path : any= (status: string) => {
|
||||
return `/investment/(tabs)/portofolio?status=${status}`;
|
||||
};
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const handleBatalkanReview = () => {
|
||||
AlertDefaultSystem({
|
||||
@@ -30,13 +33,13 @@ export default function Investment_ButtonStatusSection({
|
||||
id: id as string,
|
||||
status: "draft",
|
||||
});
|
||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil Batalkan Review",
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("draft"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -65,13 +68,13 @@ export default function Investment_ButtonStatusSection({
|
||||
id: id as string,
|
||||
status: "review",
|
||||
});
|
||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil Ajukan Review",
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("review"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -100,13 +103,13 @@ export default function Investment_ButtonStatusSection({
|
||||
id: id as string,
|
||||
status: "draft",
|
||||
});
|
||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil Update Status",
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("draft"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -135,8 +138,6 @@ export default function Investment_ButtonStatusSection({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
console.log("[RESPONSE DELETE]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
|
||||
@@ -16,10 +16,7 @@ export default function Investment_BoxDetailDocument({
|
||||
<Grid>
|
||||
<Grid.Col span={leftIcon ? 10 : 12}>
|
||||
<ClickableCustom onPress={() => router.push(href as any)}>
|
||||
<TextCustom truncate>
|
||||
{title ||
|
||||
`Judul Dokumen: Lorem, ipsum dolor sit amet consectetur adipisicing elit.`}
|
||||
</TextCustom>
|
||||
<TextCustom truncate>{title || "-"}</TextCustom>
|
||||
</ClickableCustom>
|
||||
</Grid.Col>
|
||||
{leftIcon && <Grid.Col span={2}>{leftIcon}</Grid.Col>}
|
||||
|
||||
76
screens/Invesment/Document/ScreenListDocument.tsx
Normal file
76
screens/Invesment/Document/ScreenListDocument.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { TextCustom } from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import 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 } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function Investment_ScreenListOfDocument() {
|
||||
const { id } = useLocalSearchParams();
|
||||
console.log("ID >> ", id);
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!id) return { data: [] };
|
||||
|
||||
return await apiInvestmentGetDocument({
|
||||
id: id as string,
|
||||
category: "all-document",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
onError: (error) => console.error("[ERROR] Fetch document:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Tidak ada dokumen",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item dokumen
|
||||
const renderDocumentItem = ({ item }: { item: any }) => (
|
||||
<Investment_BoxDetailDocument
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
href={`/(file)/${item.fileId}`}
|
||||
/>
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderDocumentItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
230
screens/Invesment/Document/ScreenRecapOfDocument.tsx
Normal file
230
screens/Invesment/Document/ScreenRecapOfDocument.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
DotButton,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid
|
||||
} from "@/components";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
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 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 { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function Investment_ScreenRecapOfDocument() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [openDrawerBox, setOpenDrawerBox] = useState(false);
|
||||
const [selectId, setSelectId] = useState<string | null>(null);
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!id) return { data: [] };
|
||||
|
||||
return await apiInvestmentGetDocument({
|
||||
id: id as string,
|
||||
category: "all-document",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
onError: (error) => console.error("[ERROR] Fetch document:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Tidak ada dokumen",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
const handlerDeleteDocument = async () => {
|
||||
try {
|
||||
const response = await apiInvestmentDeleteDocument({
|
||||
id: selectId as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Data berhasil dihapus",
|
||||
});
|
||||
// Hapus item dari list
|
||||
pagination.setListData((prev: any) => {
|
||||
if (!prev) return null;
|
||||
return prev.filter((item: any) => item.id !== selectId);
|
||||
});
|
||||
|
||||
pagination.onRefresh();
|
||||
setOpenDrawerBox(false);
|
||||
setSelectId(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal menghapus data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, []),
|
||||
);
|
||||
|
||||
// Render item dokumen
|
||||
const renderDocumentItem = ({ item }: { item: any }) => (
|
||||
<Investment_BoxDetailDocument
|
||||
key={item.id}
|
||||
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}`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Rekap Dokumen",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton
|
||||
onPress={() => {
|
||||
setOpenDrawer(true);
|
||||
setOpenDrawerBox(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderDocumentItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
screens/Invesment/News/BoxNews.tsx
Normal file
13
screens/Invesment/News/BoxNews.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BaseBox, Spacing, TextCustom } from "@/components";
|
||||
|
||||
export default function Investment_BoxNews({ item }: { item: { id: string; title: string } }) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox paddingBlock={5} href={`/investment/[id]/(news)/${item.id}`}>
|
||||
<Spacing height={10} />
|
||||
<TextCustom bold truncate={2}>{item.title}</TextCustom>
|
||||
<Spacing height={10} />
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
109
screens/Invesment/News/ScreenListOfNews.tsx
Normal file
109
screens/Invesment/News/ScreenListOfNews.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import Investment_BoxNews from "./BoxNews";
|
||||
|
||||
interface InvestmentListOfNewsProps {
|
||||
investmentId: string;
|
||||
}
|
||||
|
||||
export default function Investment_ScreenListOfNews({
|
||||
investmentId,
|
||||
}: InvestmentListOfNewsProps) {
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiInvestmentGetNews({
|
||||
id: investmentId,
|
||||
category: "all-news",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: 10, // Sesuaikan dengan jumlah item per halaman dari API
|
||||
dependencies: [investmentId],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [investmentId]),
|
||||
);
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<Investment_BoxNews key={index} item={item} />
|
||||
);
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada berita",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 80,
|
||||
loadingFooterText: "Memuat lebih banyak berita...",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Daftar Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tambah Berita",
|
||||
path: `/investment/${investmentId}/add-news`,
|
||||
icon: <IconPlus />,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
router.push(item.path as any);
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
110
screens/Invesment/News/ScreenRecapOfNews.tsx
Normal file
110
screens/Invesment/News/ScreenRecapOfNews.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DotButton,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
Spacing,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import Investment_BoxNews from "./BoxNews";
|
||||
|
||||
interface InvestmentRecapOfNewsProps {
|
||||
investmentId: string;
|
||||
}
|
||||
|
||||
export default function Investment_ScreenRecapOfNews({
|
||||
investmentId,
|
||||
}: InvestmentRecapOfNewsProps) {
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiInvestmentGetNews({
|
||||
id: investmentId,
|
||||
category: "all-news",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
|
||||
dependencies: [investmentId],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [investmentId]),
|
||||
);
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<Investment_BoxNews key={index} item={item} />
|
||||
);
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada berita",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 80,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Rekap Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
}}
|
||||
/>
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tambah Berita",
|
||||
path: `/investment/${investmentId}/add-news`,
|
||||
icon: <IconPlus />,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
router.push(item.path as any);
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
screens/Invesment/ScreenBursa.tsx
Normal file
67
screens/Invesment/ScreenBursa.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
FloatingButton,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import Investment_BoxBerandaSection from "@/screens/Invesment/BoxBerandaSection";
|
||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
||||
import { router } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function Investment_ScreenBursa() {
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiInvestmentGetAll({
|
||||
category: "bursa",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [],
|
||||
onError: (error) => console.error("[ERROR] Fetch investment bursa:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada investasi",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item investment
|
||||
const renderInvestmentItem = ({ item }: { item: any }) => (
|
||||
<Investment_BoxBerandaSection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
data={item}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderInvestmentItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
floatingButton={
|
||||
<FloatingButton onPress={() => router.push("/investment/create")} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
84
screens/Invesment/ScreenInvestor.tsx
Normal file
84
screens/Invesment/ScreenInvestor.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
CenterCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiInvestmentGetInvestorById } from "@/service/api-client/api-investment";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
interface InvestmentInvestorProps {
|
||||
investmentId: string;
|
||||
}
|
||||
|
||||
export default function Investment_ScreenInvestor({
|
||||
investmentId,
|
||||
}: InvestmentInvestorProps) {
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiInvestmentGetInvestorById({
|
||||
id: investmentId,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [investmentId],
|
||||
onError: (error) => {
|
||||
console.error("Error fetching investors:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<BoxWithHeaderSection>
|
||||
<StackCustom>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.Author?.Profile?.imageId}
|
||||
name={item?.Author?.username}
|
||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
||||
/>
|
||||
<CenterCustom>
|
||||
<TextCustom size="large" bold>
|
||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||
</TextCustom>
|
||||
</CenterCustom>
|
||||
</StackCustom>
|
||||
<Spacing height={10} />
|
||||
</BoxWithHeaderSection>
|
||||
);
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada investor",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 120,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
230
screens/Invesment/ScreenInvoice.tsx
Normal file
230
screens/Invesment/ScreenInvoice.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
Grid,
|
||||
InformationBox,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CopyButton from "@/components/Button/CoyButton";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import {
|
||||
apiInvestmentGetInvoice,
|
||||
apiInvestmentUpdateInvoice,
|
||||
} from "@/service/api-client/api-investment";
|
||||
import { uploadFileService } from "@/service/upload-service";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function Investment_ScreenInvoice() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any>({});
|
||||
const [image, setImage] = useState<IFileData>({
|
||||
name: "",
|
||||
uri: "",
|
||||
size: 0,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiInvestmentGetInvoice({
|
||||
id: id as string,
|
||||
category: "invoice",
|
||||
});
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmitUpdate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const responseUploadImage = await uploadFileService({
|
||||
dirId: DIRECTORY_ID.investasi_bukti_transfer,
|
||||
imageUri: image?.uri,
|
||||
});
|
||||
|
||||
if (!responseUploadImage?.data?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengunggah bukti transfer",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await apiInvestmentUpdateInvoice({
|
||||
id: id as string,
|
||||
data: {
|
||||
imageId: responseUploadImage?.data?.id,
|
||||
},
|
||||
status: "proses",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil mengunggah bukti transfer",
|
||||
});
|
||||
router.push(`/investment/${id}/(transaction-flow)/process`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<StackCustom>
|
||||
<InformationBox text="Mohon transfer ke rekening dibawah" />
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
<TextCustom>Bank</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Spacing height={10} />
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
<TextCustom>Nama Akun</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||
<Grid.Col
|
||||
span={8}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TextCustom size="xlarge" bold color="yellow">
|
||||
{data?.MasterBank?.norek}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CopyButton textToCopy={data?.MasterBank?.norek} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom>Jumlah Transaksi</TextCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
|
||||
<BaseBox backgroundColor={MainColor.soft_darkblue}>
|
||||
<Grid containerStyle={{ justifyContent: "center" }}>
|
||||
<Grid.Col
|
||||
span={8}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TextCustom size="xlarge" bold color="yellow">
|
||||
Rp. {formatCurrencyDisplay(data?.nominal)}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<CopyButton textToCopy={data?.nominal} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<TextCustom align="center">
|
||||
Upload bukti transfer anda.
|
||||
</TextCustom>
|
||||
{image ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 10,
|
||||
paddingInline: 20,
|
||||
}}
|
||||
>
|
||||
<TextCustom bold align="center" truncate>
|
||||
{image?.name}
|
||||
</TextCustom>
|
||||
</View>
|
||||
) : (
|
||||
<TextCustom align="center">
|
||||
Tidak ada gambar yang diunggah
|
||||
</TextCustom>
|
||||
)}
|
||||
<ButtonCenteredOnly
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file: any) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
icon="upload"
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!image || isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
>
|
||||
Saya Sudah Transfer
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
91
screens/Invesment/ScreenMyHolding.tsx
Normal file
91
screens/Invesment/ScreenMyHolding.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
ProgressCustom,
|
||||
StackCustom,
|
||||
TextCustom
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function Investment_ScreenMyHolding() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiInvestmentGetAll({
|
||||
category: "my-holding",
|
||||
authorId: user?.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id],
|
||||
onError: (error) => console.error("[ERROR] Fetch my holding:", error),
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [user?.id]),
|
||||
);
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<BaseBox
|
||||
paddingTop={7}
|
||||
paddingBottom={7}
|
||||
onPress={() =>
|
||||
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
||||
<TextCustom>Rp. {formatCurrencyDisplay(item?.nominal)}</TextCustom>
|
||||
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
|
||||
<ProgressCustom
|
||||
label={`${item.progress}%`}
|
||||
value={Number(item.progress)}
|
||||
size="lg"
|
||||
animated
|
||||
color="primary"
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
);
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada investasi yang dimiliki",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 120,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
102
screens/Invesment/ScreenPortofolio.tsx
Normal file
102
screens/Invesment/ScreenPortofolio.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ScrollableCustom, 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 Investment_StatusBox from "@/screens/Invesment/StatusBox";
|
||||
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function Investment_ScreenPortofolio() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish",
|
||||
);
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiInvestmentGetByStatus({
|
||||
authorId: user.id,
|
||||
status: activeCategory!,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id, activeCategory],
|
||||
onError: (error) =>
|
||||
console.error("[ERROR] Fetch investment 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: 150,
|
||||
});
|
||||
|
||||
// Render item investment
|
||||
const renderInvestmentItem = ({ item }: { item: any }) => (
|
||||
<Investment_StatusBox
|
||||
key={item.id}
|
||||
data={item}
|
||||
status={activeCategory as string}
|
||||
href={`/investment/${item.id}/${activeCategory}/detail`}
|
||||
/>
|
||||
);
|
||||
|
||||
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={tabsComponent}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderInvestmentItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
146
screens/Invesment/ScreenTransaction.tsx
Normal file
146
screens/Invesment/ScreenTransaction.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
Grid,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
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 { 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 } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Investment_ScreenTransaction() {
|
||||
const { user } = useAuth();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiInvestmentGetInvoice({
|
||||
authorId: user.id,
|
||||
category: "transaction",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id],
|
||||
onError: (error) => console.error("[ERROR] Fetch transaction:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Tidak ada transaksi",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
console.log("ID", id);
|
||||
console.log("Status", status);
|
||||
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`);
|
||||
}
|
||||
};
|
||||
|
||||
// Render item transaksi
|
||||
const renderTransactionItem = ({ item }: { item: any }) => (
|
||||
<BaseBox
|
||||
key={item.id}
|
||||
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>
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [user?.id])
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
listData={pagination.listData}
|
||||
renderItem={renderTransactionItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user