Compare commits
33 Commits
loaddata/1
...
app-header
| Author | SHA1 | Date | |
|---|---|---|---|
| f68deab8c0 | |||
| 37d2fbe48a | |||
| 57c9215771 | |||
| 4efdbd3c7b | |||
| ad32eb6fe6 | |||
| a5026cc285 | |||
| 836ef709d2 | |||
| 3bbee15c3a | |||
| ad7dbaf162 | |||
| 9c94ec0454 | |||
| 4c63485a5b | |||
| f5d09a2906 | |||
| 67070bb2f1 | |||
| fb19ec60b2 | |||
| e8f5c5b174 | |||
| 74a4d88277 | |||
| 2ad93a26a8 | |||
| 768b0caa9e | |||
| 208b0ce813 | |||
| 66e6aebf41 | |||
| 32a42d1b60 | |||
| 107d4312e1 | |||
| 4862975402 | |||
| f284e2ec02 | |||
| 1d61ad51e5 | |||
| 76845b71b4 | |||
| 97e1f50660 | |||
| 1cbe4ab330 | |||
| 42fa80c228 | |||
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 | |||
| 5c931b069c |
542
QWEN.md
542
QWEN.md
@@ -2,53 +2,68 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named "HIPMI Badung Connect" and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named **"HIPMI Badung Connect"** and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
||||||
|
|
||||||
### Key Technologies
|
### Key Technologies
|
||||||
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
|
- **Framework**: Expo (v54.0.0) with React Native (v0.81.5)
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
- **Architecture**: File-based routing with Expo Router
|
- **Architecture**: File-based routing with Expo Router
|
||||||
- **State Management**: Context API
|
- **State Management**: Context API (AuthContext)
|
||||||
- **UI Components**: React Native Paper, custom components
|
- **UI Components**: React Native Paper, custom components
|
||||||
- **Maps Integration**: Mapbox Maps for React Native
|
- **Maps Integration**: Maplibre Maps for React Native (`@maplibre/maplibre-react-native` v10.4.2)
|
||||||
- **Push Notifications**: React Native Firebase Messaging
|
- **Push Notifications**: React Native Firebase Messaging
|
||||||
- **Build System**: Metro bundler
|
- **Build System**: Metro bundler
|
||||||
|
- **Package Manager**: Bun
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
```
|
```
|
||||||
hipmi-mobile/
|
hipmi-mobile/
|
||||||
├── app/ # Main application screens and routing
|
├── app/ # Main application screens and routing (Expo Router)
|
||||||
│ ├── _layout.tsx # Root layout component
|
│ ├── _layout.tsx # Root layout component
|
||||||
│ ├── index.tsx # Entry point (Login screen)
|
│ ├── index.tsx # Entry point (Login screen)
|
||||||
│ └── ...
|
│ └── (application)/ # Main app screens
|
||||||
|
│ ├── admin/ # Admin panel screens
|
||||||
|
│ ├── (user)/ # User screens
|
||||||
|
│ └── ...
|
||||||
├── components/ # Reusable UI components
|
├── components/ # Reusable UI components
|
||||||
|
│ ├── _ShareComponent/ # Shared components (NewWrapper, Admin components)
|
||||||
|
│ ├── _Icon/ # Icon components
|
||||||
|
│ └── ...
|
||||||
├── context/ # State management (AuthContext)
|
├── context/ # State management (AuthContext)
|
||||||
├── screens/ # Screen components organized by feature
|
├── screens/ # Screen components organized by feature
|
||||||
│ ├── Admin/ # Admin panel screens
|
│ ├── Admin/ # Admin panel screens
|
||||||
|
│ │ ├── Donation/ # Donation management screens
|
||||||
|
│ │ ├── Voting/ # Voting management screens
|
||||||
|
│ │ ├── Event/ # Event management screens
|
||||||
|
│ │ └── ...
|
||||||
│ ├── Authentication/ # Login, registration flows
|
│ ├── Authentication/ # Login, registration flows
|
||||||
│ ├── Collaboration/ # Collaboration features
|
│ ├── RootLayout/ # Root layout components
|
||||||
│ ├── Event/ # Event management
|
|
||||||
│ ├── Forum/ # Forum functionality
|
|
||||||
│ ├── Home/ # Home screen
|
|
||||||
│ ├── Maps/ # Map integration
|
|
||||||
│ ├── Profile/ # User profile
|
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── assets/ # Images, icons, and static assets
|
|
||||||
├── constants/ # Constants and configuration values
|
|
||||||
├── hooks/ # Custom React hooks
|
|
||||||
├── lib/ # Utility libraries
|
|
||||||
├── navigation/ # Navigation configuration
|
|
||||||
├── service/ # API services and business logic
|
├── service/ # API services and business logic
|
||||||
|
│ ├── api-admin/ # Admin API endpoints
|
||||||
|
│ ├── api-client/ # Client API endpoints
|
||||||
|
│ └── api-config.ts # Axios configuration
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
│ ├── use-pagination.tsx # Pagination hook
|
||||||
|
│ └── ...
|
||||||
|
├── helpers/ # Helper functions
|
||||||
|
│ ├── paginationHelpers.tsx # Pagination UI helpers
|
||||||
|
│ └── ...
|
||||||
├── types/ # TypeScript type definitions
|
├── types/ # TypeScript type definitions
|
||||||
└── utils/ # Helper functions
|
├── utils/ # Utility functions
|
||||||
|
├── constants/ # Constants and configuration values
|
||||||
|
├── styles/ # Global styles
|
||||||
|
├── assets/ # Images, icons, and static assets
|
||||||
|
└── docs/ # Documentation files
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js (with bun as the package manager)
|
- **Node.js**: v18+ with Bun package manager
|
||||||
- Expo CLI
|
- **Expo CLI**: Installed globally or via npx
|
||||||
- iOS Simulator or Android Emulator (for native builds)
|
- **iOS**: Xcode (macOS only) for iOS simulator/builds
|
||||||
|
- **Android**: Android Studio for Android emulator/builds
|
||||||
|
|
||||||
### Setup and Development
|
### Setup and Development
|
||||||
|
|
||||||
@@ -60,110 +75,441 @@ hipmi-mobile/
|
|||||||
2. **Run Development Server**
|
2. **Run Development Server**
|
||||||
```bash
|
```bash
|
||||||
bun run start
|
bun run start
|
||||||
```
|
# or
|
||||||
Or use the shorthand:
|
|
||||||
```bash
|
|
||||||
bunx expo start
|
bunx expo start
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Platform-Specific Commands**
|
3. **Platform-Specific Commands**
|
||||||
- iOS: `bun run ios` or `bunx expo start --ios`
|
```bash
|
||||||
- Android: `bun run android` or `bunx expo start --android`
|
# iOS Simulator
|
||||||
- Web: `bun run web` or `bunx expo start --web`
|
bun run ios
|
||||||
|
# or
|
||||||
|
bunx expo start --ios
|
||||||
|
|
||||||
|
# Android Emulator
|
||||||
|
bun run android
|
||||||
|
# or
|
||||||
|
bunx expo start --android
|
||||||
|
|
||||||
|
# Web Browser
|
||||||
|
bun run web
|
||||||
|
# or
|
||||||
|
bunx expo start --web
|
||||||
|
```
|
||||||
|
|
||||||
4. **Linting**
|
4. **Linting**
|
||||||
```bash
|
```bash
|
||||||
bun run lint
|
bun run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Build Commands
|
||||||
The application uses environment variables defined in the app.config.js file:
|
|
||||||
- `API_BASE_URL`: Base URL for API endpoints
|
|
||||||
- `BASE_URL`: Base application URL
|
|
||||||
- `DEEP_LINK_URL`: URL for deep linking functionality
|
|
||||||
|
|
||||||
### EAS Build Configuration
|
#### EAS Build (Production)
|
||||||
The project uses Expo Application Services (EAS) for building and deploying:
|
```bash
|
||||||
- Development builds with development client
|
# Production build (App Store / Play Store)
|
||||||
- Preview builds for internal distribution
|
eas build --profile production
|
||||||
- Production builds for app stores
|
|
||||||
|
|
||||||
## Features and Functionality
|
# Preview build (Internal distribution)
|
||||||
|
eas build --profile preview
|
||||||
|
|
||||||
The application appears to include several key modules:
|
# Development build (Development client)
|
||||||
- **Authentication**: Login, registration, and verification flows
|
eas build --profile development
|
||||||
- **Admin Panel**: Administrative functions
|
```
|
||||||
- **Collaboration**: Tools for member collaboration
|
|
||||||
- **Events**: Event management and calendar
|
#### Local Native Builds
|
||||||
- **Forum**: Discussion forums
|
```bash
|
||||||
- **Maps**: Location-based services with Mapbox integration
|
# Generate native folders (iOS & Android)
|
||||||
- **Donations**: Donation functionality
|
npx expo prebuild
|
||||||
- **Job Board**: Employment opportunities
|
|
||||||
- **Investment**: Investment-related features
|
# iOS specific
|
||||||
- **Voting**: Voting systems
|
bunx expo prebuild --platform ios
|
||||||
- **Portfolio**: Member portfolio showcase
|
open ios/HIPMIBadungConnect.xcworkspace
|
||||||
- **Notifications**: Push notifications via Firebase
|
|
||||||
|
# Android specific
|
||||||
|
bunx expo prebuild --platform android
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version Management
|
||||||
|
```bash
|
||||||
|
# Patch version update
|
||||||
|
npm version patch
|
||||||
|
|
||||||
|
# Update iOS build number
|
||||||
|
bunx expo prebuild --platform ios
|
||||||
|
|
||||||
|
# Update Android version code
|
||||||
|
bunx expo prebuild --platform android
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android Debugging
|
||||||
|
```bash
|
||||||
|
# List connected devices
|
||||||
|
adb devices
|
||||||
|
|
||||||
|
# Install APK to device/emulator
|
||||||
|
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
# Install to specific device
|
||||||
|
adb -s <device_id> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the project root with:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_BASE_URL=https://your-api-base-url.com
|
||||||
|
BASE_URL=https://your-app-url.com
|
||||||
|
DEEP_LINK_URL=hipmimobile://
|
||||||
|
```
|
||||||
|
|
||||||
|
These are loaded in `app.config.js` and accessible via `Constants.expoConfig.extra`.
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### 1. Separation of Concerns
|
||||||
|
|
||||||
|
**Route Files** (`app/`) should be minimal (max 5 lines):
|
||||||
|
```typescript
|
||||||
|
import { Admin_ScreenXXX } from "@/screens/Admin/XXX/ScreenXXX";
|
||||||
|
|
||||||
|
export default function AdminXXX() {
|
||||||
|
return <Admin_ScreenXXX />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screen Components** (`screens/`) contain all business logic:
|
||||||
|
```typescript
|
||||||
|
export function Admin_ScreenXXX() {
|
||||||
|
// Logic, hooks, state management
|
||||||
|
return <NewWrapper ... />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Pagination Pattern
|
||||||
|
|
||||||
|
Using `usePagination` hook with infinite scroll:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pagination = usePagination({
|
||||||
|
fetchFunction: async (page, searchQuery) => {
|
||||||
|
const response = await apiXXX({ page: String(page) });
|
||||||
|
if (response.success) {
|
||||||
|
return { data: response.data };
|
||||||
|
}
|
||||||
|
return { data: [] };
|
||||||
|
},
|
||||||
|
pageSize: PAGINATION_DEFAULT_TAKE, // 10
|
||||||
|
searchQuery: search,
|
||||||
|
dependencies: [dependency],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ListEmptyComponent, ListFooterComponent } =
|
||||||
|
createPaginationComponents({
|
||||||
|
loading: pagination.loading,
|
||||||
|
refreshing: pagination.refreshing,
|
||||||
|
listData: pagination.listData,
|
||||||
|
emptyMessage: "Belum ada data",
|
||||||
|
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Wrapper Components
|
||||||
|
|
||||||
|
**NewWrapper** (preferred for lists):
|
||||||
|
```typescript
|
||||||
|
<NewWrapper
|
||||||
|
listData={pagination.listData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
headerComponent={headerComponent}
|
||||||
|
ListEmptyComponent={ListEmptyComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
onEndReached={pagination.loadMore}
|
||||||
|
refreshControl={<RefreshControl ... />}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**AdminBasicBox** (for card layouts):
|
||||||
|
```typescript
|
||||||
|
<AdminBasicBox
|
||||||
|
onPress={() => router.push(`/path/${item.id}`)}
|
||||||
|
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||||
|
>
|
||||||
|
<StackCustom gap={0}>
|
||||||
|
<GridSpan_4_8 label="Label" value={<TextCustom>Value</TextCustom>} />
|
||||||
|
</StackCustom>
|
||||||
|
</AdminBasicBox>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. API Service Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// service/api-admin/api-xxx.ts
|
||||||
|
export async function apiXXX({ page = "1" }: { page?: string }) {
|
||||||
|
try {
|
||||||
|
const response = await apiConfig.get(`/mobile/admin/xxx?page=${page}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: All list APIs should support pagination with `page` parameter (default: "1").
|
||||||
|
|
||||||
|
### 5. Authentication Flow
|
||||||
|
|
||||||
|
Managed by `AuthContext`:
|
||||||
|
- `loginWithNomor()` - Send phone number, receive OTP
|
||||||
|
- `validateOtp()` - Validate OTP, get token
|
||||||
|
- `registerUser()` - Register new user
|
||||||
|
- `logout()` - Clear session and logout
|
||||||
|
- `userData()` - Fetch user data by token
|
||||||
|
|
||||||
## Development Conventions
|
## Development Conventions
|
||||||
|
|
||||||
### Coding Standards
|
### Coding Standards
|
||||||
- TypeScript is used throughout the project for type safety
|
- **TypeScript**: Strict mode enabled
|
||||||
- Component-based architecture with reusable components
|
- **Naming**:
|
||||||
- Context API for state management
|
- Components: PascalCase (`Admin_ScreenDonationStatus`)
|
||||||
- File-based routing with Expo Router
|
- Files: PascalCase for components (`ScreenDonationStatus.tsx`)
|
||||||
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
- Variables: camelCase
|
||||||
|
- Constants: UPPER_SNAKE_CASE
|
||||||
|
- **Path Aliases**: `@/*` maps to project root
|
||||||
|
- **Imports**: Group imports by type (components, hooks, services, etc.)
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
```typescript
|
||||||
|
// 1. Imports (grouped)
|
||||||
|
import { ... } from "@/components";
|
||||||
|
import { ... } from "@/hooks";
|
||||||
|
import { ... } from "@/service";
|
||||||
|
|
||||||
|
// 2. Types/Interfaces
|
||||||
|
interface Props { ... }
|
||||||
|
|
||||||
|
// 3. Main Component
|
||||||
|
export function ComponentName() {
|
||||||
|
// State
|
||||||
|
// Hooks
|
||||||
|
// Functions
|
||||||
|
// Render
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- Linting is configured with ESLint
|
- Linting: `bun run lint`
|
||||||
- Standard Expo linting configuration is used
|
- No formal test suite configured yet
|
||||||
|
|
||||||
### Security
|
### Git Workflow
|
||||||
- Firebase is integrated for authentication and messaging
|
- Feature branches: `feature/xxx` or `fixed-admin/xxx`
|
||||||
- Camera and location permissions are properly configured
|
- Commit messages: Clear and descriptive
|
||||||
- Deep linking is secured with app domain associations
|
- Use CHANGE_LOG.md for tracking changes
|
||||||
|
|
||||||
## Key Dependencies
|
## Key Features
|
||||||
|
|
||||||
### Core Dependencies
|
### Authentication
|
||||||
- `@react-navigation/*`: Navigation solution for React Native
|
- Phone number login with OTP
|
||||||
- `@react-native-firebase/*`: Firebase integration for React Native
|
- User registration
|
||||||
- `@rnmapbox/maps`: Mapbox integration for React Native
|
- Terms & Conditions acceptance
|
||||||
- `expo-router`: File-based routing for Expo applications
|
- Session persistence with AsyncStorage
|
||||||
- `react-native-paper`: Material Design components for React Native
|
|
||||||
- `react-native-toast-message`: Toast notifications
|
|
||||||
- `react-native-otp-entry`: OTP input components
|
|
||||||
- `react-native-qrcode-svg`: QR code generation
|
|
||||||
|
|
||||||
### Development Dependencies
|
### Admin Module
|
||||||
- `@types/*`: TypeScript type definitions
|
- **Dashboard**: Overview and statistics
|
||||||
- `eslint-config-expo`: Expo-specific ESLint configuration
|
- **User Access**: User management
|
||||||
- `typescript`: Type checking
|
- **Event**: Event CRUD with status management
|
||||||
|
- **Voting**: Voting management (publish/review/reject)
|
||||||
|
- **Donation**: Donation management with categories and transaction tracking
|
||||||
|
- **Collaboration**: Collaboration requests
|
||||||
|
- **Investment**: Investment management
|
||||||
|
- **Maps**: Location-based features
|
||||||
|
- **App Information**: Bank and business field management
|
||||||
|
|
||||||
## Platform Support
|
### User Module
|
||||||
|
- **Home**: Main dashboard
|
||||||
|
- **Forum**: Discussion forums
|
||||||
|
- **Profile**: User profile management
|
||||||
|
- **Portfolio**: Member portfolio
|
||||||
|
- **Notifications**: Push notifications via Firebase
|
||||||
|
|
||||||
The application is configured to support:
|
## API Configuration
|
||||||
- **iOS**: With tablet support and proper permissions
|
|
||||||
- **Android**: With adaptive icons and intent filters for deep linking
|
|
||||||
- **Web**: Static output configuration for web deployment
|
|
||||||
|
|
||||||
## Special Configurations
|
### Base URLs
|
||||||
|
```typescript
|
||||||
|
// From app.config.js extra
|
||||||
|
API_BASE_URL: process.env.API_BASE_URL
|
||||||
|
BASE_URL: process.env.BASE_URL
|
||||||
|
DEEP_LINK_URL: process.env.DEEP_LINK_URL
|
||||||
|
```
|
||||||
|
|
||||||
### iOS Configuration
|
### Axios Interceptor
|
||||||
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
All API calls use `apiConfig` with automatic token injection:
|
||||||
- Supports tablets
|
```typescript
|
||||||
- Google Services integration
|
apiConfig.interceptors.request.use(async (config) => {
|
||||||
- Location permission handling
|
const token = await AsyncStorage.getItem("authToken");
|
||||||
- Associated domains for deep linking
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Android Configuration
|
## Platform Configuration
|
||||||
- Package name: `com.bip.hipmimobileapp`
|
|
||||||
- Adaptive icons
|
|
||||||
- Edge-to-edge display enabled
|
|
||||||
- Intent filters for HTTPS deep linking
|
|
||||||
- Google Services integration
|
|
||||||
|
|
||||||
### Maps Integration
|
### iOS
|
||||||
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
- **Bundle ID**: `com.anonymous.hipmi-mobile`
|
||||||
|
- **Build Number**: 21
|
||||||
|
- **Google Services**: Configured
|
||||||
|
- **Associated Domains**: `applinks:cld-dkr-staging-hipmi.wibudev.com`
|
||||||
|
- **Tablet Support**: Enabled
|
||||||
|
|
||||||
### Push Notifications
|
### Android
|
||||||
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
- **Package**: `com.bip.hipmimobileapp`
|
||||||
|
- **Version Code**: 4
|
||||||
|
- **Google Services**: Configured (`google-services.json`)
|
||||||
|
- **Deep Links**: HTTPS intent filters configured
|
||||||
|
- **Edge-to-Edge**: Enabled
|
||||||
|
|
||||||
|
### Web
|
||||||
|
- **Output**: Static
|
||||||
|
- **Bundler**: Metro
|
||||||
|
|
||||||
|
## Special Integrations
|
||||||
|
|
||||||
|
### Firebase
|
||||||
|
- Authentication
|
||||||
|
- Push Notifications (FCM)
|
||||||
|
- Configured for both iOS and Android
|
||||||
|
|
||||||
|
### Maplibre
|
||||||
|
- Map integration via `@maplibre/maplibre-react-native`
|
||||||
|
- Location permissions configured
|
||||||
|
|
||||||
|
### Deep Linking
|
||||||
|
- Scheme: `hipmimobile://`
|
||||||
|
- HTTPS: `cld-dkr-hipmi-stg.wibudev.com`
|
||||||
|
- Configured for both platforms
|
||||||
|
|
||||||
|
### Camera
|
||||||
|
- Camera and microphone permissions
|
||||||
|
- QR code generation support
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding a New Admin Screen
|
||||||
|
|
||||||
|
1. **Create Screen Component** (`screens/Admin/Feature/ScreenXXX.tsx`):
|
||||||
|
```typescript
|
||||||
|
export function Admin_ScreenXXX() {
|
||||||
|
const pagination = usePagination({...});
|
||||||
|
const renderItem = useCallback(...);
|
||||||
|
const headerComponent = useMemo(...);
|
||||||
|
|
||||||
|
return <NewWrapper ... />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Box Component** (optional, for custom item rendering):
|
||||||
|
```typescript
|
||||||
|
export default function Admin_BoxXXX({ item }: { item: any }) {
|
||||||
|
return (
|
||||||
|
<AdminBasicBox onPress={() => router.push(...)}>
|
||||||
|
...
|
||||||
|
</AdminBasicBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update API** (add pagination if needed):
|
||||||
|
```typescript
|
||||||
|
export async function apiXXX({ page = "1" }: { page?: string }) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create Route File** (`app/(application)/admin/feature/xxx.tsx`):
|
||||||
|
```typescript
|
||||||
|
import { Admin_ScreenXXX } from "@/screens/Admin/Feature/ScreenXXX";
|
||||||
|
|
||||||
|
export default function AdminXXX() {
|
||||||
|
return <Admin_ScreenXXX />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating API Endpoints
|
||||||
|
|
||||||
|
1. Add function in appropriate service file
|
||||||
|
2. Include `page` parameter for list endpoints
|
||||||
|
3. Use `apiConfig` axios instance
|
||||||
|
4. Handle errors properly
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Issues
|
||||||
|
```bash
|
||||||
|
# Clean and rebuild
|
||||||
|
rm -rf node_modules
|
||||||
|
bun install
|
||||||
|
bunx expo prebuild --clean
|
||||||
|
|
||||||
|
# iOS specific
|
||||||
|
cd ios && pod install && cd ..
|
||||||
|
|
||||||
|
# Android specific
|
||||||
|
cd android && ./gradlew clean && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Issues
|
||||||
|
```bash
|
||||||
|
# Clear Expo cache
|
||||||
|
bunx expo start -c
|
||||||
|
|
||||||
|
# Clear Metro cache
|
||||||
|
bunx expo start --clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Issues
|
||||||
|
```bash
|
||||||
|
# Reinstall dependencies
|
||||||
|
rm -rf node_modules bun.lock
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS Maplibre Crash Fix
|
||||||
|
|
||||||
|
When using Maplibre MapView on iOS, prevent "Attempt to recycle a mounted view" crash:
|
||||||
|
|
||||||
|
1. **Always render PointAnnotation** (not conditional)
|
||||||
|
2. **Use opacity for visibility** instead of conditional rendering
|
||||||
|
3. **Avoid key prop changes** that force remounting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Stable PointAnnotation
|
||||||
|
<PointAnnotation
|
||||||
|
coordinate={annotationCoordinate} // Always rendered
|
||||||
|
...
|
||||||
|
>
|
||||||
|
<View style={{ opacity: selectedLocation ? 1 : 0 }}>
|
||||||
|
<SelectedLocationMarker />
|
||||||
|
</View>
|
||||||
|
</PointAnnotation>
|
||||||
|
|
||||||
|
// ❌ BAD: Conditional rendering causes crash
|
||||||
|
{selectedLocation && (
|
||||||
|
<PointAnnotation coordinate={selectedLocation} ... />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Files
|
||||||
|
|
||||||
|
- `docs/CHANGE_LOG.md` - Change log for recent updates
|
||||||
|
- `docs/hipmi-note.md` - Build and deployment notes
|
||||||
|
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Expo Documentation](https://docs.expo.dev/)
|
||||||
|
- [React Native Documentation](https://reactnative.dev/)
|
||||||
|
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
|
||||||
|
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||||
|
- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native)
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ packagingOptions {
|
|||||||
applicationId 'com.bip.hipmimobileapp'
|
applicationId 'com.bip.hipmimobileapp'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 4
|
versionCode 1
|
||||||
versionName "1.0.1"
|
versionName "1.0.2"
|
||||||
|
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true" data-generated="true">
|
<intent-filter android:autoVerify="true" data-generated="true">
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<data android:scheme="https" android:host="cld-dkr-staging-hipmi.wibudev.com" android:pathPrefix="/"/>
|
<data android:scheme="https" android:host="cld-dkr-hipmi-stg.wibudev.com" android:pathPrefix="/"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
// app.config.js
|
// app.config.js
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
|
// const isDev = process.env.NODE_ENV === "development";
|
||||||
|
// const isStaging = process.env.NEXT_PUBLIC_ENV === "staging";
|
||||||
|
// const isProd = process.env.NEXT_PUBLIC_ENV === "production";
|
||||||
|
|
||||||
|
// Domain berdasarkan environment
|
||||||
|
// const domain = isDev
|
||||||
|
// ? "localhost:3000"
|
||||||
|
// : isStaging
|
||||||
|
// ? "cld-dkr-hipmi-stg.wibudev.com"
|
||||||
|
// : "hipmi.muku.id"; // Production domain
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "HIPMI Badung Connect",
|
name: "HIPMI Badung Connect",
|
||||||
slug: "hipmi-mobile",
|
slug: "hipmi-mobile",
|
||||||
version: "1.0.1",
|
version: "1.0.2",
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
icon: "./assets/images/icon.png",
|
icon: "./assets/images/icon.png",
|
||||||
scheme: "hipmimobile",
|
scheme: "hipmimobile",
|
||||||
@@ -20,8 +31,10 @@ export default {
|
|||||||
NSLocationWhenInUseUsageDescription:
|
NSLocationWhenInUseUsageDescription:
|
||||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||||
},
|
},
|
||||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
associatedDomains: [
|
||||||
buildNumber: "20",
|
"applinks:cld-dkr-hipmi-stg.wibudev.com",
|
||||||
|
],
|
||||||
|
buildNumber: "5",
|
||||||
},
|
},
|
||||||
|
|
||||||
android: {
|
android: {
|
||||||
@@ -32,7 +45,7 @@ export default {
|
|||||||
},
|
},
|
||||||
edgeToEdgeEnabled: true,
|
edgeToEdgeEnabled: true,
|
||||||
package: "com.bip.hipmimobileapp",
|
package: "com.bip.hipmimobileapp",
|
||||||
versionCode: 4,
|
versionCode: 1,
|
||||||
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
|
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
|
||||||
intentFilters: [
|
intentFilters: [
|
||||||
{
|
{
|
||||||
@@ -41,7 +54,7 @@ export default {
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
scheme: "https",
|
scheme: "https",
|
||||||
host: "cld-dkr-staging-hipmi.wibudev.com",
|
host: "cld-dkr-hipmi-stg.wibudev.com",
|
||||||
pathPrefix: "/",
|
pathPrefix: "/",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -77,7 +90,6 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"@rnmapbox/maps",
|
|
||||||
"@react-native-firebase/app",
|
"@react-native-firebase/app",
|
||||||
[
|
[
|
||||||
"expo-notifications",
|
"expo-notifications",
|
||||||
@@ -87,6 +99,7 @@ export default {
|
|||||||
iosDisplayInForeground: true,
|
iosDisplayInForeground: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"@maplibre/maplibre-react-native",
|
||||||
],
|
],
|
||||||
|
|
||||||
experiments: {
|
experiments: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import PdfViewer from "@/components/_ShareComponent/PdfViewer";
|
import PdfViewer from "@/components/_ShareComponent/PdfViewer";
|
||||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||||
import { Stack, useLocalSearchParams } from "expo-router";
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
@@ -7,13 +8,12 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|||||||
export default function FileScreen() {
|
export default function FileScreen() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const url = API_STRORAGE.GET({ fileId: id as string });
|
const url = API_STRORAGE.GET({ fileId: id as string });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "File",
|
header: () => <AppHeader title="File" left={<BackButton />} />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SafeAreaView style={{ flex: 1 }} edges={["bottom"]}>
|
<SafeAreaView style={{ flex: 1 }} edges={["bottom"]}>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function TakePicture() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable onPress={pickImage}>
|
<Pressable onPress={pickImage}>
|
||||||
<AntDesign name="folderopen" size={32} color="white" />
|
<AntDesign name="folder-open" size={32} color="white" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconPlus } from "@/components/_Icon";
|
import { IconPlus } from "@/components/_Icon";
|
||||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
import { HeaderStyles } from "@/styles/header-styles";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
|
|
||||||
export default function UserLayout() {
|
export default function UserLayout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack screenOptions={HeaderStyles}>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="delete-account"
|
name="delete-account"
|
||||||
options={{
|
options={{
|
||||||
title: "Hapus Akun",
|
header: () => <AppHeader title="Hapus Akun" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -47,8 +46,7 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="user-search/index"
|
name="user-search/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Pencarian Pengguna",
|
header: () => <AppHeader title="Pencarian Pengguna" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -71,10 +69,18 @@ export default function UserLayout() {
|
|||||||
|
|
||||||
{/* ========== Event Section ========= */}
|
{/* ========== Event Section ========= */}
|
||||||
|
|
||||||
|
{/* <Stack.Screen
|
||||||
|
name="event/(tabs)"
|
||||||
|
options={{
|
||||||
|
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="event/(tabs)"
|
name="event/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Event",
|
title: "Event",
|
||||||
|
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
|
||||||
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
|
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
|
||||||
// headerLeft: () => (
|
// headerLeft: () => (
|
||||||
// <LeftButtonCustom path="/(application)/(user)/home" />
|
// <LeftButtonCustom path="/(application)/(user)/home" />
|
||||||
@@ -85,32 +91,28 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="event/create"
|
name="event/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Event",
|
header: () => <AppHeader title="Tambah Event" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="event/detail/[id]"
|
name="event/detail/[id]"
|
||||||
options={{
|
options={{
|
||||||
title: "Event Detail",
|
header: () => <AppHeader title="Event Detail" left={<LeftButtonCustom />} />,
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="event/[id]/edit"
|
name="event/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Event",
|
header: () => <AppHeader title="Edit Event" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="event/[id]/list-of-participants"
|
name="event/[id]/list-of-participants"
|
||||||
options={{
|
options={{
|
||||||
title: "Daftar peserta",
|
header: () => <AppHeader title="Daftar peserta" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* ========== End Event Section ========= */}
|
{/* ========== End Event Section ========= */}
|
||||||
@@ -119,22 +121,19 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/(tabs)"
|
name="collaboration/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Collaboration",
|
header: () => <AppHeader title="Collaboration" left={<BackButton path="/home" />} />,
|
||||||
headerLeft: () => <BackButton path="/home" />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/create"
|
name="collaboration/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Proyek",
|
header: () => <AppHeader title="Tambah Proyek" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/[id]/list-of-participants"
|
name="collaboration/[id]/list-of-participants"
|
||||||
options={{
|
options={{
|
||||||
title: "Daftar Partisipan",
|
header: () => <AppHeader title="Daftar Partisipan" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <Stack.Screen
|
{/* <Stack.Screen
|
||||||
@@ -147,22 +146,19 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/[id]/edit"
|
name="collaboration/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Proyek",
|
header: () => <AppHeader title="Edit Proyek" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/[id]/create-pacticipants"
|
name="collaboration/[id]/create-pacticipants"
|
||||||
options={{
|
options={{
|
||||||
title: "Ajukan Partisipasi",
|
header: () => <AppHeader title="Ajukan Partisipasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="collaboration/[id]/select-of-participants"
|
name="collaboration/[id]/select-of-participants"
|
||||||
options={{
|
options={{
|
||||||
title: "Pilih Partisipan",
|
header: () => <AppHeader title="Pilih Partisipan" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -172,29 +168,25 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="voting/create"
|
name="voting/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Voting",
|
header: () => <AppHeader title="Tambah Voting" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="voting/(tabs)"
|
name="voting/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Voting",
|
header: () => <AppHeader title="Voting" left={<BackButton path="/home" />} />,
|
||||||
headerLeft: () => <BackButton path="/home" />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="voting/[id]/edit"
|
name="voting/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Voting",
|
header: () => <AppHeader title="Edit Voting" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="voting/[id]/list-of-contributor"
|
name="voting/[id]/list-of-contributor"
|
||||||
options={{
|
options={{
|
||||||
title: "Daftar Kontributor",
|
header: () => <AppHeader title="Daftar Kontributor" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -204,8 +196,7 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="crowdfunding/index"
|
name="crowdfunding/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Crowdfunding",
|
header: () => <AppHeader title="Crowdfunding" left={<BackButton path="/home" />} />,
|
||||||
headerLeft: () => <BackButton path="/home" />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -215,103 +206,95 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/(tabs)"
|
name="investment/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Investasi",
|
header: () => <AppHeader title="Investasi" left={<BackButton path="/crowdfunding" />} />,
|
||||||
headerLeft: () => <BackButton path="/crowdfunding" />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/create"
|
name="investment/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Investasi",
|
header: () => <AppHeader title="Tambah Investasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/index"
|
name="investment/[id]/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Investasi",
|
header: () => <AppHeader title="Detail Investasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/edit"
|
name="investment/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Investasi",
|
header: () => <AppHeader title="Edit Investasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/edit-prospectus"
|
name="investment/[id]/edit-prospectus"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Prospektus",
|
header: () => <AppHeader title="Edit Prospektus" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(document)/list-of-document"
|
name="investment/[id]/(document)/list-of-document"
|
||||||
options={{
|
options={{
|
||||||
title: "Daftar Dokumen",
|
header: () => <AppHeader title="Daftar Dokumen" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(document)/add-document"
|
name="investment/[id]/(document)/add-document"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Dokumen",
|
header: () => <AppHeader title="Tambah Dokumen" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(document)/edit-document"
|
name="investment/[id]/(document)/edit-document"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Dokumen",
|
header: () => <AppHeader title="Edit Dokumen" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(news)/add-news"
|
name="investment/[id]/(news)/add-news"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Berita",
|
header: () => <AppHeader title="Tambah Berita" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/investor"
|
name="investment/[id]/investor"
|
||||||
options={{
|
options={{
|
||||||
title: "Investor",
|
header: () => <AppHeader title="Investor" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/index"
|
name="investment/[id]/(transaction-flow)/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Pembelian Saham",
|
header: () => <AppHeader title="Pembelian Saham" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/select-bank"
|
name="investment/[id]/(transaction-flow)/select-bank"
|
||||||
options={{
|
options={{
|
||||||
title: "Pilih Bank",
|
header: () => <AppHeader title="Pilih Bank" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/invoice"
|
name="investment/[id]/(transaction-flow)/invoice"
|
||||||
options={{
|
options={{
|
||||||
title: "Invoice",
|
header: () => (
|
||||||
headerLeft: () => (
|
<AppHeader
|
||||||
<Ionicons
|
title="Invoice"
|
||||||
name="close"
|
left={
|
||||||
size={ICON_SIZE_SMALL}
|
<Ionicons
|
||||||
color={MainColor.yellow}
|
name="close"
|
||||||
onPress={() =>
|
size={ICON_SIZE_SMALL}
|
||||||
router.navigate(`/investment/(tabs)/transaction`)
|
color={MainColor.yellow}
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate(`/investment/(tabs)/transaction`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -320,14 +303,18 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/process"
|
name="investment/[id]/(transaction-flow)/process"
|
||||||
options={{
|
options={{
|
||||||
title: "Proses",
|
header: () => (
|
||||||
headerLeft: () => (
|
<AppHeader
|
||||||
<Ionicons
|
title="Proses"
|
||||||
name="close"
|
left={
|
||||||
size={ICON_SIZE_SMALL}
|
<Ionicons
|
||||||
color={MainColor.yellow}
|
name="close"
|
||||||
onPress={() =>
|
size={ICON_SIZE_SMALL}
|
||||||
router.navigate(`/investment/(tabs)/transaction`)
|
color={MainColor.yellow}
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate(`/investment/(tabs)/transaction`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -336,23 +323,20 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/success"
|
name="investment/[id]/(transaction-flow)/success"
|
||||||
options={{
|
options={{
|
||||||
title: "Transaksi Berhasil",
|
header: () => <AppHeader title="Transaksi Berhasil" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(transaction-flow)/failed"
|
name="investment/[id]/(transaction-flow)/failed"
|
||||||
options={{
|
options={{
|
||||||
title: "Transaksi Gagal",
|
header: () => <AppHeader title="Transaksi Gagal" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="investment/[id]/(my-holding)/[id]"
|
name="investment/[id]/(my-holding)/[id]"
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Saham Saya",
|
header: () => <AppHeader title="Detail Saham Saya" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* ========== End Investment Section ========= */}
|
{/* ========== End Investment Section ========= */}
|
||||||
@@ -361,122 +345,111 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/(tabs)"
|
name="donation/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Donasi",
|
header: () => <AppHeader title="Donasi" left={<BackButton path="/crowdfunding" />} />,
|
||||||
headerLeft: () => <BackButton path="/crowdfunding" />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/create"
|
name="donation/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Donasi",
|
header: () => <AppHeader title="Tambah Donasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/create-story"
|
name="donation/create-story"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Donasi",
|
header: () => <AppHeader title="Tambah Donasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/edit"
|
name="donation/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Donasi",
|
header: () => <AppHeader title="Edit Donasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/edit-story"
|
name="donation/[id]/edit-story"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Donasi",
|
header: () => <AppHeader title="Edit Donasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/edit-rekening"
|
name="donation/[id]/edit-rekening"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Rekening",
|
header: () => <AppHeader title="Edit Rekening" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/detail-story"
|
name="donation/[id]/detail-story"
|
||||||
options={{
|
options={{
|
||||||
title: "Cerita Penggalang",
|
header: () => <AppHeader title="Cerita Penggalang" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/infromation-fundrising"
|
name="donation/[id]/infromation-fundrising"
|
||||||
options={{
|
options={{
|
||||||
title: "Informasi Penggalang Dana",
|
header: () => <AppHeader title="Informasi Penggalang Dana" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/list-of-donatur"
|
name="donation/[id]/list-of-donatur"
|
||||||
options={{
|
options={{
|
||||||
title: "Daftar Donatur",
|
header: () => <AppHeader title="Daftar Donatur" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/fund-disbursement"
|
name="donation/[id]/fund-disbursement"
|
||||||
options={{
|
options={{
|
||||||
title: "Pencairan Dana",
|
header: () => <AppHeader title="Pencairan Dana" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(news)/recap-of-news"
|
name="donation/[id]/(news)/recap-of-news"
|
||||||
options={{
|
options={{
|
||||||
title: "Rekap Kabar",
|
header: () => <AppHeader title="Rekap Kabar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(news)/add-news"
|
name="donation/[id]/(news)/add-news"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Berita",
|
header: () => <AppHeader title="Tambah Berita" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(news)/[news]/edit-news"
|
name="donation/[id]/(news)/[news]/edit-news"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Berita",
|
header: () => <AppHeader title="Edit Berita" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/index"
|
name="donation/[id]/(transaction-flow)/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Donasi",
|
header: () => <AppHeader title="Donasi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/select-bank"
|
name="donation/[id]/(transaction-flow)/select-bank"
|
||||||
options={{
|
options={{
|
||||||
title: "Pilih Bank",
|
header: () => <AppHeader title="Pilih Bank" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/[invoiceId]/invoice"
|
name="donation/[id]/(transaction-flow)/[invoiceId]/invoice"
|
||||||
options={{
|
options={{
|
||||||
title: "Invoice",
|
header: () => (
|
||||||
headerLeft: () => (
|
<AppHeader
|
||||||
<Ionicons
|
title="Invoice"
|
||||||
name="close"
|
left={
|
||||||
size={ICON_SIZE_SMALL}
|
<Ionicons
|
||||||
color={MainColor.yellow}
|
name="close"
|
||||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
size={ICON_SIZE_SMALL}
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -484,13 +457,17 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/[invoiceId]/process"
|
name="donation/[id]/(transaction-flow)/[invoiceId]/process"
|
||||||
options={{
|
options={{
|
||||||
title: "Proses",
|
header: () => (
|
||||||
headerLeft: () => (
|
<AppHeader
|
||||||
<Ionicons
|
title="Proses"
|
||||||
name="close"
|
left={
|
||||||
size={ICON_SIZE_SMALL}
|
<Ionicons
|
||||||
color={MainColor.yellow}
|
name="close"
|
||||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
size={ICON_SIZE_SMALL}
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -498,55 +475,51 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/[invoiceId]/success"
|
name="donation/[id]/(transaction-flow)/[invoiceId]/success"
|
||||||
options={{
|
options={{
|
||||||
title: "Donasi Berhasil",
|
header: () => <AppHeader title="Donasi Berhasil" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="donation/[id]/(transaction-flow)/[invoiceId]/failed"
|
name="donation/[id]/(transaction-flow)/[invoiceId]/failed"
|
||||||
options={{
|
options={{
|
||||||
title: "Donasi Gagal",
|
header: () => <AppHeader title="Donasi Gagal" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ========== End Donation Section ========= */}
|
{/* ========== End Donation Section ========= */}
|
||||||
|
|
||||||
{/* ========== Job Section ========= */}
|
{/* ========== Job Section ========= */}
|
||||||
|
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="job/create"
|
name="job/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Job",
|
header: () => <AppHeader title="Tambah Job" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="job/(tabs)"
|
name="job/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Job Vacancy",
|
title: "Job Vacancy",
|
||||||
// headerLeft: () => <BackButton path="/home" />,
|
|
||||||
// NOTE: headerLeft di pindahkan ke Tabs Layout
|
// NOTE: headerLeft di pindahkan ke Tabs Layout
|
||||||
|
header: () => <AppHeader title="Job Vacancy" left={<BackButton path="/home" />} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="job/[id]/index"
|
name="job/[id]/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Job",
|
header: () => <AppHeader title="Detail Job" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="job/[id]/edit"
|
name="job/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Job",
|
header: () => <AppHeader title="Edit Job" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="job/[id]/archive"
|
name="job/[id]/archive"
|
||||||
options={{
|
options={{
|
||||||
title: "Arsip Job",
|
header: () => <AppHeader title="Arsip Job" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -556,78 +529,67 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/create"
|
name="forum/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Diskusi",
|
header: () => <AppHeader title="Tambah Diskusi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/edit"
|
name="forum/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Diskusi",
|
header: () => <AppHeader title="Edit Diskusi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/forumku"
|
name="forum/[id]/forumku"
|
||||||
options={{
|
options={{
|
||||||
title: "Forumku",
|
header: () => <AppHeader title="Forumku" left={<BackButton icon={"close"} />} />,
|
||||||
headerLeft: () => <BackButton icon={"close"} />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/index"
|
name="forum/[id]/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Detail",
|
header: () => <AppHeader title="Detail" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/report-commentar"
|
name="forum/[id]/report-commentar"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporkan Komentar",
|
header: () => <AppHeader title="Laporkan Komentar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/other-report-commentar"
|
name="forum/[id]/other-report-commentar"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporkan Komentar",
|
header: () => <AppHeader title="Laporkan Komentar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/report-posting"
|
name="forum/[id]/report-posting"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporkan Diskusi",
|
header: () => <AppHeader title="Laporkan Diskusi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/other-report-posting"
|
name="forum/[id]/other-report-posting"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporkan Diskusi",
|
header: () => <AppHeader title="Laporkan Diskusi" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/terms"
|
name="forum/terms"
|
||||||
options={{
|
options={{
|
||||||
title: "Syarat & Ketentuan Forum",
|
header: () => <AppHeader title="Syarat & Ketentuan Forum" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/preview-report-posting"
|
name="forum/[id]/preview-report-posting"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporan Postingan",
|
header: () => <AppHeader title="Laporan Postingan" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="forum/[id]/preview-report-comment"
|
name="forum/[id]/preview-report-comment"
|
||||||
options={{
|
options={{
|
||||||
title: "Laporan Komentar",
|
header: () => <AppHeader title="Laporan Komentar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -635,29 +597,25 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="maps/index"
|
name="maps/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Maps",
|
header: () => <AppHeader title="Maps" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="maps/create"
|
name="maps/create"
|
||||||
options={{
|
options={{
|
||||||
title: "Tambah Maps",
|
header: () => <AppHeader title="Tambah Maps" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="maps/[id]/edit"
|
name="maps/[id]/edit"
|
||||||
options={{
|
options={{
|
||||||
title: "Edit Maps",
|
header: () => <AppHeader title="Edit Maps" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="maps/[id]/custom-pin"
|
name="maps/[id]/custom-pin"
|
||||||
options={{
|
options={{
|
||||||
title: "Custom Pin Maps",
|
header: () => <AppHeader title="Custom Pin Maps" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -665,8 +623,7 @@ export default function UserLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="marketplace/index"
|
name="marketplace/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Market Place",
|
header: () => <AppHeader title="Market Place" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
|
import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
|
||||||
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
@@ -40,8 +41,7 @@ export default function CollaborationRoomInfo() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Info`,
|
header: () => <AppHeader title="Info" left={<BackButton />} />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
|
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
|
||||||
@@ -12,14 +13,18 @@ export default function CollaborationRoomChat() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Proyek ${detail}`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title={`Proyek ${detail}`}
|
||||||
<Feather
|
left={<BackButton />}
|
||||||
name="info"
|
right={
|
||||||
size={ICON_SIZE_SMALL}
|
<Feather
|
||||||
color={MainColor.yellow}
|
name="info"
|
||||||
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
|
size={ICON_SIZE_SMALL}
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
ViewWrapper
|
ViewWrapper
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||||
import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
|
import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
@@ -38,10 +39,14 @@ export default function CollaborationDetailParticipant() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Proyek",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Detail Proyek"
|
||||||
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
|
left={<BackButton />}
|
||||||
|
right={
|
||||||
|
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper
|
ViewWrapper
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconEdit } from "@/components/_Icon";
|
import { IconEdit } from "@/components/_Icon";
|
||||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||||
import {
|
import {
|
||||||
@@ -66,9 +67,13 @@ export default function CollaborationDetailProjectMain() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Proyek Saya",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
title="Proyek Saya"
|
||||||
|
left={<BackButton />}
|
||||||
|
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||||
import {
|
import {
|
||||||
@@ -74,10 +75,14 @@ export default function CollaborationDetail() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Proyek",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Detail Proyek"
|
||||||
<DotButton onPress={() => setOpenDrawerMenu(true)} />
|
left={<BackButton />}
|
||||||
|
right={
|
||||||
|
<DotButton onPress={() => setOpenDrawerMenu(true)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconEdit } from "@/components/_Icon";
|
import { IconEdit } from "@/components/_Icon";
|
||||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -57,12 +58,17 @@ export default function DonationNews() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Kabar",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Detail Kabar"
|
||||||
user?.id === data?.authorId && (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
),
|
user?.id === data?.authorId && (
|
||||||
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NewWrapper,
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconEdit, IconNews } from "@/components/_Icon";
|
import { IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
@@ -97,14 +98,19 @@ export default function DonasiDetailStatus() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail ${_.startCase(status as string)}`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title={`Detail ${_.startCase(status as string)}`}
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
) : status === "publish" ? (
|
status === "draft" ? (
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
) : null,
|
) : status === "publish" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NewWrapper
|
<NewWrapper
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconNews } from "@/components/_Icon";
|
import { IconNews } from "@/components/_Icon";
|
||||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -90,12 +91,17 @@ export default function DonasiDetailBeranda() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail Donasi`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Detail Donasi"
|
||||||
user?.id === data?.Author?.id ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
) : null,
|
user?.id === data?.Author?.id ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NewWrapper footerComponent={buttonSection}>
|
<NewWrapper footerComponent={buttonSection}>
|
||||||
|
|||||||
@@ -4,36 +4,34 @@ import {
|
|||||||
IconHome,
|
IconHome,
|
||||||
IconStatus,
|
IconStatus,
|
||||||
} from "@/components/_Icon";
|
} from "@/components/_Icon";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||||
import { TabsStyles } from "@/styles/tabs-styles";
|
import { TabsStyles } from "@/styles/tabs-styles";
|
||||||
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
|
import { router, Tabs, useLocalSearchParams } from "expo-router";
|
||||||
import { useLayoutEffect } from "react";
|
|
||||||
|
|
||||||
export default function EventTabsLayout() {
|
export default function EventTabsLayout() {
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const { from, category } = useLocalSearchParams<{
|
const { from, category } = useLocalSearchParams<{
|
||||||
from?: string;
|
from?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
console.log("from", from);
|
|
||||||
console.log("category", category);
|
|
||||||
|
|
||||||
// Atur header secara dinamis
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => (
|
|
||||||
<BackButtonFromNotification
|
|
||||||
from={from as string}
|
|
||||||
category={category as string}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}, [from, router, navigation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs screenOptions={TabsStyles}>
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
...TabsStyles,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Event"
|
||||||
|
left={
|
||||||
|
<BackButtonFromNotification
|
||||||
|
from={from as string}
|
||||||
|
category={category as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
|
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
|
||||||
@@ -81,12 +82,17 @@ export default function EventDetailStatus() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail ${status === "publish" ? "" : status}`,
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title={`Detail ${status === "publish" ? "" : status}`}
|
||||||
status === "draft" ? (
|
left={<LeftButtonCustom />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
) : null,
|
status === "draft" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
apiEventConfirmationAction,
|
apiEventConfirmationAction,
|
||||||
@@ -60,7 +61,7 @@ export default function UserEventConfirmation() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
checkTokenAndDataParticipants() || console.log("Token is null");
|
checkTokenAndDataParticipants() || console.log("Token is null");
|
||||||
}, [token, id, user?.id])
|
}, [token, id, user?.id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkTokenAndDataParticipants = async () => {
|
const checkTokenAndDataParticipants = async () => {
|
||||||
@@ -113,7 +114,7 @@ export default function UserEventConfirmation() {
|
|||||||
confirmationStart,
|
confirmationStart,
|
||||||
confirmationEnd,
|
confirmationEnd,
|
||||||
null,
|
null,
|
||||||
"[]"
|
"[]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- [4] Status waktu event (untuk pesan UI) ---
|
// --- [4] Status waktu event (untuk pesan UI) ---
|
||||||
@@ -218,9 +219,14 @@ export default function UserEventConfirmation() {
|
|||||||
if (isWithinConfirmationWindow) {
|
if (isWithinConfirmationWindow) {
|
||||||
if (konfirmasi === false) {
|
if (konfirmasi === false) {
|
||||||
return (
|
return (
|
||||||
<TamplateBox data={data}>
|
// <TamplateBox data={data}>
|
||||||
<TamplateText text="Konfirmasi Kehadiran" />
|
// <TamplateText text="Konfirmasi Kehadiran" />
|
||||||
</TamplateBox>
|
// </TamplateBox>
|
||||||
|
<UserParticipan_And_DuringEvent
|
||||||
|
id={data.id}
|
||||||
|
userId={user?.id as string}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -260,18 +266,20 @@ export default function UserEventConfirmation() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Konfirmasi Event",
|
header: () => (
|
||||||
// headerLeft: () => (
|
<AppHeader
|
||||||
// <Ionicons
|
title="Konfirmasi Event"
|
||||||
// name="arrow-back"
|
left={
|
||||||
// size={20}
|
<Ionicons
|
||||||
// color={MainColor.yellow}
|
name="arrow-back"
|
||||||
// onPress={() =>
|
size={20}
|
||||||
// router.navigate("/(application)/(user)/event/create")
|
color={MainColor.yellow}
|
||||||
// }
|
onPress={() => router.navigate("/")}
|
||||||
// />
|
/>
|
||||||
// ),
|
}
|
||||||
}}
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>{handlerReturn()}</ViewWrapper>
|
<ViewWrapper>{handlerReturn()}</ViewWrapper>
|
||||||
</>
|
</>
|
||||||
@@ -497,7 +505,6 @@ const UserNotParticipan_And_DuringEvent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 🟡 ZONA ACARA BERLANGSUN
|
// 🟡 ZONA ACARA BERLANGSUN
|
||||||
// User sudah terdaftar & Event sedang berlangsung & user harus konfirmasi
|
// User sudah terdaftar & Event sedang berlangsung & user harus konfirmasi
|
||||||
const UserParticipan_And_DuringEvent = ({
|
const UserParticipan_And_DuringEvent = ({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
||||||
@@ -49,9 +50,13 @@ export default function EventDetailContribution() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail kontribusi`,
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
title="Detail kontribusi"
|
||||||
|
left={<LeftButtonCustom />}
|
||||||
|
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
||||||
@@ -44,9 +45,13 @@ export default function EventDetailHistory() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail riwayat`,
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
title="Detail riwayat"
|
||||||
|
left={<LeftButtonCustom />}
|
||||||
|
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
@@ -156,9 +157,13 @@ export default function EventDetailPublish() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Event Publish`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton onPress={() => router.back()} />,
|
<AppHeader
|
||||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
title="Event Publish"
|
||||||
|
left={<BackButton onPress={() => router.back()} />}
|
||||||
|
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -1,92 +1,153 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { StackCustom, ViewWrapper } from "@/components";
|
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
|
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
|
||||||
import HeaderBell from "@/screens/Home/HeaderBell";
|
import HeaderBell from "@/screens/Home/HeaderBell";
|
||||||
|
import { stylesHome } from "@/screens/Home/homeViewStyle";
|
||||||
import Home_ImageSection from "@/screens/Home/imageSection";
|
import Home_ImageSection from "@/screens/Home/imageSection";
|
||||||
import TabSection from "@/screens/Home/tabSection";
|
import TabSection from "@/screens/Home/tabSection";
|
||||||
import { tabsHome } from "@/screens/Home/tabsList";
|
import { tabsHome } from "@/screens/Home/tabsList";
|
||||||
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
|
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
|
||||||
|
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||||
import { apiUser } from "@/service/api-client/api-user";
|
import { apiUser } from "@/service/api-client/api-user";
|
||||||
import { apiVersion } from "@/service/api-config";
|
import { apiVersion } from "@/service/api-config";
|
||||||
|
import { GStyles } from "@/styles/global-styles";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
|
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { RefreshControl } from "react-native";
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
export default function Application() {
|
export default function Application() {
|
||||||
const { token, user, userData } = useAuth();
|
const { token, user, userData } = useAuth();
|
||||||
const [data, setData] = useState<any>();
|
const [data, setData] = useState<any>();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const { syncUnreadCount } = useNotificationStore();
|
const { syncUnreadCount } = useNotificationStore();
|
||||||
|
const [listData, setListData] = useState<any[] | null>(null);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
|
onLoadDataJob();
|
||||||
checkVersion();
|
checkVersion();
|
||||||
userData(token as string);
|
userData(token as string).catch((error) => {
|
||||||
|
console.log("[ERROR userData]", error?.message);
|
||||||
|
console.log("[ERROR userData Response]", error?.response?.data);
|
||||||
|
});
|
||||||
syncUnreadCount();
|
syncUnreadCount();
|
||||||
}, [user?.id, token]),
|
}, [user?.id, token]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData() {
|
async function onLoadData() {
|
||||||
const response = await apiUser(user?.id as string);
|
try {
|
||||||
console.log(
|
const response = await apiUser(user?.id as string);
|
||||||
"[Profile ID]>>",
|
setData(response.data);
|
||||||
JSON.stringify(response?.data?.Profile?.id, null, 2),
|
} catch (error: any) {
|
||||||
);
|
console.log("[ERROR onLoadData]", error?.message);
|
||||||
|
console.log("[ERROR Response]", error?.response?.data);
|
||||||
setData(response.data);
|
// Set data tetap agar UI tidak stuck di loading
|
||||||
|
setData(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onLoadDataJob = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiJobGetAll({
|
||||||
|
category: "beranda",
|
||||||
|
});
|
||||||
|
const result = response.data
|
||||||
|
.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
|
)
|
||||||
|
.slice(0, 2);
|
||||||
|
setListData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR]", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
const checkVersion = async () => {
|
const checkVersion = async () => {
|
||||||
const response = await apiVersion();
|
try {
|
||||||
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
|
const response = await apiVersion();
|
||||||
|
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("[ERROR checkVersion]", error?.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRefresh = useCallback(() => {
|
const onRefresh = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
onLoadData();
|
onLoadData();
|
||||||
|
onLoadDataJob();
|
||||||
checkVersion();
|
checkVersion();
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// if (user && user?.termsOfServiceAccepted === false) {
|
|
||||||
// console.log("User is not accept term service");
|
|
||||||
// return <Redirect href={`/terms-agreement`} />;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (data && data?.active === false) {
|
if (data && data?.active === false) {
|
||||||
console.log("User is not active");
|
console.warn("User is not active");
|
||||||
return <Redirect href={`/waiting-room`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/waiting-room`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data?.Profile === null) {
|
if (data && data?.Profile === null) {
|
||||||
console.log("Profile is null");
|
console.warn("Profile is null");
|
||||||
return <Redirect href={`/profile/create`} />;
|
return (
|
||||||
|
<BasicWrapper>
|
||||||
|
<Redirect href={`/profile/create`} />
|
||||||
|
</BasicWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (data && data?.masterUserRoleId !== "1") {
|
||||||
|
// console.log("User is not admin");
|
||||||
|
// return (
|
||||||
|
// <BasicWrapper>
|
||||||
|
// <Redirect href={`/admin/dashboard`} />
|
||||||
|
// </BasicWrapper>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `HIPMI`,
|
header: () => (
|
||||||
headerLeft: () => (
|
<AppHeader
|
||||||
<Ionicons
|
title="HIPMI"
|
||||||
name="search"
|
showBack={false}
|
||||||
size={20}
|
left={
|
||||||
color={MainColor.yellow}
|
data ? (
|
||||||
onPress={() => {
|
<Ionicons
|
||||||
router.push("/user-search");
|
name="search"
|
||||||
}}
|
size={20}
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/user-search");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CustomSkeleton height={30} width={30} radius={100} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
data ? (
|
||||||
|
<HeaderBell />
|
||||||
|
) : (
|
||||||
|
<CustomSkeleton height={30} width={30} radius={100} />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
headerRight: () => <HeaderBell />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewWrapper
|
<ViewWrapper
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
@@ -97,24 +158,51 @@ export default function Application() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footerComponent={
|
footerComponent={
|
||||||
<TabSection
|
data && data ? (
|
||||||
tabs={tabsHome({
|
<TabSection
|
||||||
acceptedForumTermsAt: data?.acceptedForumTermsAt,
|
tabs={tabsHome({
|
||||||
profileId: data?.Profile?.id,
|
acceptedForumTermsAt: data?.acceptedForumTermsAt,
|
||||||
})}
|
profileId: data?.Profile?.id,
|
||||||
/>
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={GStyles.tabBar}>
|
||||||
|
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
|
||||||
|
{Array.from({ length: 4 }).map((e, index) => (
|
||||||
|
<CustomSkeleton
|
||||||
|
key={index}
|
||||||
|
height={40}
|
||||||
|
width={40}
|
||||||
|
radius={100}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
|
|
||||||
Test Notif
|
|
||||||
</ButtonCustom> */}
|
|
||||||
|
|
||||||
<Home_ImageSection />
|
<Home_ImageSection />
|
||||||
|
|
||||||
<Home_FeatureSection />
|
{data && data ? (
|
||||||
|
<Home_FeatureSection />
|
||||||
|
) : (
|
||||||
|
<View style={stylesHome.gridContainer}>
|
||||||
|
{Array.from({ length: 4 }).map((item, index) => (
|
||||||
|
<CustomSkeleton
|
||||||
|
key={index}
|
||||||
|
style={stylesHome.gridItem}
|
||||||
|
radius={50}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Home_BottomFeatureSection />
|
{data ? (
|
||||||
|
<Home_BottomFeatureSection listData={listData} />
|
||||||
|
) : (
|
||||||
|
<CustomSkeleton height={200} />
|
||||||
|
)}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -30,13 +31,13 @@ export default function InvestmentDetailHolding() {
|
|||||||
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
|
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
|
||||||
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id, status])
|
}, [id, status])
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiInvestmentGetInvoice({
|
const response = await apiInvestmentGetInvoice({
|
||||||
@@ -44,7 +45,7 @@ export default function InvestmentDetailHolding() {
|
|||||||
authorId: user?.id,
|
authorId: user?.id,
|
||||||
category: "invoice",
|
category: "invoice",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[DATA]", JSON.stringify(response.data, null, 2));
|
console.log("[DATA]", JSON.stringify(response.data, null, 2));
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -76,14 +77,19 @@ export default function InvestmentDetailHolding() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail ${_.startCase(status as string)}`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title={`Detail ${_.startCase(status as string)}`}
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
right={
|
||||||
) : status === "publish" ? (
|
status === "draft" ? (
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||||
) : null,
|
) : status === "publish" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
@@ -56,12 +57,17 @@ export default function InvestmentNews() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Berita",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Detail Berita"
|
||||||
user?.id === data?.authorId && (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
),
|
user?.id === data?.authorId && (
|
||||||
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -106,14 +107,19 @@ export default function InvestmentDetailStatus() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail ${_.startCase(status as string)}`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title={`Detail ${_.startCase(status as string)}`}
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
right={
|
||||||
) : status === "publish" ? (
|
status === "draft" ? (
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||||
) : null,
|
) : status === "publish" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MenuDrawerDynamicGrid,
|
MenuDrawerDynamicGrid,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -105,14 +106,19 @@ export default function InvestmentDetail() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail ${_.startCase(status as string)}`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title={`Detail ${_.startCase(status as string)}`}
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
right={
|
||||||
) : status === "publish" ? (
|
status === "draft" ? (
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||||
) : null,
|
) : status === "publish" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconHome, IconStatus } from "@/components/_Icon";
|
import { IconHome, IconStatus } from "@/components/_Icon";
|
||||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||||
import { TabsStyles } from "@/styles/tabs-styles";
|
import { TabsStyles } from "@/styles/tabs-styles";
|
||||||
@@ -7,31 +8,30 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import {
|
import {
|
||||||
router,
|
router,
|
||||||
Tabs,
|
Tabs,
|
||||||
useLocalSearchParams,
|
useLocalSearchParams
|
||||||
useNavigation
|
|
||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
import { useLayoutEffect } from "react";
|
|
||||||
|
|
||||||
export default function JobTabsLayout() {
|
export default function JobTabsLayout() {
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const { from, category } = useLocalSearchParams<{
|
const { from, category } = useLocalSearchParams<{
|
||||||
from?: string;
|
from?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Atur header secara dinamis
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => (
|
|
||||||
<BackButtonFromNotification from={from as string} category={category as string} />
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}, [from, router, navigation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs screenOptions={TabsStyles}>
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
...TabsStyles,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Job Vacancy"
|
||||||
|
left={
|
||||||
|
<BackButtonFromNotification from={from as string} category={category as string} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconEdit } from "@/components/_Icon";
|
import { IconEdit } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
import ReportBox from "@/components/Box/ReportBox";
|
||||||
@@ -58,12 +59,17 @@ export default function JobDetailStatus() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Detail"
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
right={
|
||||||
) : null,
|
status === "draft" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -1,244 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit";
|
||||||
import {
|
|
||||||
BoxButtonOnFooter,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
ButtonCustom,
|
|
||||||
InformationBox,
|
|
||||||
LandscapeFrameUploaded,
|
|
||||||
Spacing,
|
|
||||||
TextInputCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
|
||||||
import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps";
|
|
||||||
import { uploadFileService } from "@/service/upload-service";
|
|
||||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { StyleSheet, View } from "react-native";
|
|
||||||
import MapView, { LatLng, Marker } from "react-native-maps";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
const defaultRegion = {
|
|
||||||
latitude: -8.737109,
|
|
||||||
longitude: 115.1756897,
|
|
||||||
latitudeDelta: 0.1,
|
|
||||||
longitudeDelta: 0.1,
|
|
||||||
};
|
|
||||||
export default function MapsEdit() {
|
export default function MapsEdit() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Maps_ScreenMapsEdit />;
|
||||||
const [data, setData] = useState<any | null>({
|
|
||||||
id: "",
|
|
||||||
namePin: "",
|
|
||||||
latitude: "",
|
|
||||||
longitude: "",
|
|
||||||
imageId: "",
|
|
||||||
});
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
|
|
||||||
const [image, setImage] = useState<IFileData | null>(null);
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMapsGetOne({ id: id as string });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData({
|
|
||||||
id: response.data.id,
|
|
||||||
namePin: response.data.namePin,
|
|
||||||
latitude: response.data.latitude,
|
|
||||||
longitude: response.data.longitude,
|
|
||||||
imageId: response.data.imageId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMapPress = (event: any) => {
|
|
||||||
const { latitude, longitude } = event.nativeEvent.coordinate;
|
|
||||||
const location = { latitude, longitude };
|
|
||||||
setSelectedLocation(location);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
let newData: any;
|
|
||||||
if (!data.namePin) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Nama pin harus diisi",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newData = {
|
|
||||||
namePin: data?.namePin,
|
|
||||||
latitude: selectedLocation?.latitude || data?.latitude,
|
|
||||||
longitude: selectedLocation?.longitude || data?.longitude,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
if (image) {
|
|
||||||
const responseUpload = await uploadFileService({
|
|
||||||
dirId: DIRECTORY_ID.map_image,
|
|
||||||
imageUri: image?.uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!responseUpload?.data?.id) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal mengunggah gambar",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageId = responseUpload?.data?.id;
|
|
||||||
|
|
||||||
newData = {
|
|
||||||
namePin: data?.namePin,
|
|
||||||
latitude: selectedLocation?.latitude,
|
|
||||||
longitude: selectedLocation?.longitude,
|
|
||||||
newImageId: imageId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseUpdate = await apiMapsUpdate({
|
|
||||||
id: data?.id,
|
|
||||||
data: newData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!responseUpdate.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal mengupdate map",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Map berhasil diupdate",
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonFooter = (
|
|
||||||
<BoxButtonOnFooter>
|
|
||||||
<ButtonCustom
|
|
||||||
disabled={!data.namePin}
|
|
||||||
onPress={handleSubmit}
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ButtonCustom>
|
|
||||||
</BoxButtonOnFooter>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewWrapper footerComponent={buttonFooter}>
|
|
||||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
|
||||||
|
|
||||||
<View style={[styles.container, { height: 400 }]}>
|
|
||||||
<MapView
|
|
||||||
style={styles.map}
|
|
||||||
initialRegion={
|
|
||||||
data?.latitude && data?.longitude
|
|
||||||
? {
|
|
||||||
latitude: data?.latitude,
|
|
||||||
longitude: data?.longitude,
|
|
||||||
latitudeDelta: 0.1,
|
|
||||||
longitudeDelta: 0.1,
|
|
||||||
}
|
|
||||||
: defaultRegion
|
|
||||||
}
|
|
||||||
onPress={handleMapPress}
|
|
||||||
showsUserLocation={true}
|
|
||||||
showsMyLocationButton={true}
|
|
||||||
loadingEnabled={true}
|
|
||||||
loadingIndicatorColor="#666"
|
|
||||||
loadingBackgroundColor="#f0f0f0"
|
|
||||||
>
|
|
||||||
{selectedLocation ? (
|
|
||||||
<Marker
|
|
||||||
coordinate={selectedLocation}
|
|
||||||
title="Lokasi Dipilih"
|
|
||||||
description={`Lat: ${selectedLocation.latitude.toFixed(
|
|
||||||
6
|
|
||||||
)}, Lng: ${selectedLocation.longitude.toFixed(6)}`}
|
|
||||||
pinColor="red"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Marker
|
|
||||||
coordinate={defaultRegion}
|
|
||||||
title="Lokasi Dipilih"
|
|
||||||
description={`Lat: ${defaultRegion.latitude.toFixed(
|
|
||||||
6
|
|
||||||
)}, Lng: ${defaultRegion.longitude.toFixed(6)}`}
|
|
||||||
pinColor="red"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MapView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TextInputCustom
|
|
||||||
required
|
|
||||||
label="Nama Pin"
|
|
||||||
placeholder="Masukkan nama pin maps"
|
|
||||||
value={data?.namePin}
|
|
||||||
onChangeText={(value) => setData({ ...data, namePin: value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
|
||||||
<LandscapeFrameUploaded
|
|
||||||
image={
|
|
||||||
image
|
|
||||||
? image?.uri
|
|
||||||
: API_IMAGE.GET({ fileId: data?.imageId as string })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
icon="upload"
|
|
||||||
onPress={() => {
|
|
||||||
pickFile({
|
|
||||||
allowedType: "image",
|
|
||||||
setImageUri(file) {
|
|
||||||
setImage(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
<Spacing height={50} />
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: "#f5f5f5",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
map: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,143 +1,9 @@
|
|||||||
import {
|
import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
|
||||||
BaseBox,
|
|
||||||
BoxButtonOnFooter,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
ButtonCustom,
|
|
||||||
InformationBox,
|
|
||||||
LandscapeFrameUploaded,
|
|
||||||
Spacing,
|
|
||||||
TextInputCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import MapSelected from "@/components/Map/MapSelected";
|
|
||||||
import DIRECTORY_ID from "@/constants/directory-id";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { apiMapsCreate } from "@/service/api-client/api-maps";
|
|
||||||
import { uploadFileService } from "@/service/upload-service";
|
|
||||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { LatLng } from "react-native-maps";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function MapsCreate() {
|
export default function MapsCreate() {
|
||||||
const { user } = useAuth();
|
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
|
|
||||||
const [name, setName] = useState<string>("");
|
|
||||||
const [image, setImage] = useState<IFileData | null>(null);
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
let newData: any;
|
|
||||||
newData = {
|
|
||||||
authorId: user?.id,
|
|
||||||
portofolioId: id,
|
|
||||||
namePin: name,
|
|
||||||
latitude: selectedLocation?.latitude,
|
|
||||||
longitude: selectedLocation?.longitude,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
const responseUpload = await uploadFileService({
|
|
||||||
dirId: DIRECTORY_ID.map_image,
|
|
||||||
imageUri: image?.uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!responseUpload?.data?.id) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal mengunggah gambar",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageId = responseUpload?.data?.id;
|
|
||||||
|
|
||||||
newData = {
|
|
||||||
authorId: user?.id,
|
|
||||||
portofolioId: id,
|
|
||||||
namePin: name,
|
|
||||||
latitude: selectedLocation?.latitude,
|
|
||||||
longitude: selectedLocation?.longitude,
|
|
||||||
imageId: imageId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await apiMapsCreate({
|
|
||||||
data: newData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal menambahkan map",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Map berhasil ditambahkan",
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonFooter = (
|
|
||||||
<BoxButtonOnFooter>
|
|
||||||
<ButtonCustom
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={!selectedLocation || name === ""}
|
|
||||||
onPress={handleSubmit}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ButtonCustom>
|
|
||||||
</BoxButtonOnFooter>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper footerComponent={buttonFooter}>
|
<>
|
||||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
<Maps_ScreenMapsCreate />
|
||||||
|
</>
|
||||||
<BaseBox>
|
|
||||||
<MapSelected
|
|
||||||
selectedLocation={selectedLocation as any}
|
|
||||||
setSelectedLocation={setSelectedLocation}
|
|
||||||
/>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<TextInputCustom
|
|
||||||
required
|
|
||||||
label="Nama Pin"
|
|
||||||
placeholder="Masukkan nama pin maps"
|
|
||||||
value={name}
|
|
||||||
onChangeText={setName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Spacing height={50} />
|
|
||||||
|
|
||||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
|
||||||
<LandscapeFrameUploaded image={image?.uri} />
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
icon="upload"
|
|
||||||
onPress={() => {
|
|
||||||
pickFile({
|
|
||||||
allowedType: "image",
|
|
||||||
setImageUri(file) {
|
|
||||||
setImage(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
<Spacing height={50} />
|
|
||||||
</ViewWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import MapsView from "@/screens/Maps/MapsView";
|
|
||||||
import MapsView2 from "@/screens/Maps/MapsView2";
|
import MapsView2 from "@/screens/Maps/MapsView2";
|
||||||
import { Text, View } from "react-native";
|
|
||||||
|
|
||||||
export interface LocationItem {
|
export interface LocationItem {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -13,8 +11,14 @@ export interface LocationItem {
|
|||||||
export default function Maps() {
|
export default function Maps() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MapsView />
|
{/* <Stack.Screen
|
||||||
{/* <MapsView2 />, */}
|
options={{
|
||||||
|
title: "Maps",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
{/* {Platform.OS === "ios" ? <MapsView /> : <MapsView2 />} */}
|
||||||
|
<MapsView2 />
|
||||||
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
|
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,365 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
import { Admin_ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
AvatarComp,
|
|
||||||
BaseBox,
|
|
||||||
ButtonCenteredOnly,
|
|
||||||
CenterCustom,
|
|
||||||
Grid,
|
|
||||||
InformationBox,
|
|
||||||
NewWrapper,
|
|
||||||
SelectCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextAreaCustom,
|
|
||||||
TextCustom,
|
|
||||||
TextInputCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconPlus } from "@/components/_Icon";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
|
||||||
import DUMMY_IMAGE from "@/constants/dummy-image-value";
|
|
||||||
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
|
|
||||||
import {
|
|
||||||
apiMasterBidangBisnis,
|
|
||||||
apiMasterSubBidangBisnis,
|
|
||||||
} from "@/service/api-client/api-master";
|
|
||||||
import {
|
|
||||||
IMasterBidangBisnis,
|
|
||||||
IMasterSubBidangBisnis,
|
|
||||||
} from "@/types/Type-Master";
|
|
||||||
import pickImage from "@/utils/pickImage";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { Text, TouchableOpacity, View } from "react-native";
|
|
||||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
|
||||||
import { Avatar } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function PortofolioCreate() {
|
export default function PortofolioCreate() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenPortofolioCreate />;
|
||||||
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
|
|
||||||
const [inputValue, setInputValue] = useState<string>("");
|
|
||||||
const [data, setData] = useState({
|
|
||||||
namaBisnis: "",
|
|
||||||
masterBidangBisnisId: "",
|
|
||||||
alamatKantor: "",
|
|
||||||
tlpn: "",
|
|
||||||
deskripsi: "",
|
|
||||||
});
|
|
||||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
|
|
||||||
const [subBidangBisnis, setSubBidangBisnis] = useState<
|
|
||||||
IMasterSubBidangBisnis[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
|
|
||||||
const [listSubBidangSelected, setListSubBidangSelected] = useState([
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [dataMedsos, setDataMedsos] = useState({
|
|
||||||
facebook: "",
|
|
||||||
twitter: "",
|
|
||||||
instagram: "",
|
|
||||||
youtube: "",
|
|
||||||
tiktok: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
|
|
||||||
|
|
||||||
function handleInputValue(phoneNumber: string) {
|
|
||||||
setInputValue(phoneNumber);
|
|
||||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
|
||||||
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
|
|
||||||
const realNumber = callingCode + fixNumber;
|
|
||||||
setData({ ...data, tlpn: realNumber });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelectedCountry(country: ICountry) {
|
|
||||||
setSelectedCountry(country);
|
|
||||||
}
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadMaster();
|
|
||||||
onLoadMasterSubBidangBisnis();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadMaster = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMasterBidangBisnis();
|
|
||||||
setBidangBisnis(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
setBidangBisnis([]);
|
|
||||||
console.log("Error onLoadMasterBidangBisnis", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoadMasterSubBidangBisnis = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMasterSubBidangBisnis({});
|
|
||||||
setSubBidangBisnis(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
setSubBidangBisnis([]);
|
|
||||||
console.log("Error onLoadMasterBidangBisnis", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
|
|
||||||
const selectedList = subBidangBisnis?.filter(
|
|
||||||
(item) => (item?.masterBidangBisnisId as any) === id
|
|
||||||
);
|
|
||||||
setSelectedSubBidang(selectedList as any[]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NewWrapper
|
|
||||||
footerComponent={
|
|
||||||
<Portofolio_ButtonCreate
|
|
||||||
id={id as string}
|
|
||||||
data={data}
|
|
||||||
dataMedsos={dataMedsos}
|
|
||||||
imageUri={imageUri}
|
|
||||||
subBidangSelected={listSubBidangSelected}
|
|
||||||
isLoadingCreate={isLoadingCreate}
|
|
||||||
setIsLoadingCreate={setIsLoadingCreate}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* <TextCustom>Portofolio Create {id}</TextCustom> */}
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<InformationBox text="Lengkapi data bisnis anda." />
|
|
||||||
<TextInputCustom
|
|
||||||
required
|
|
||||||
label="Nama Bisnis"
|
|
||||||
placeholder="Masukkan nama bisnis"
|
|
||||||
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectCustom
|
|
||||||
label="Bidang Usaha"
|
|
||||||
required
|
|
||||||
data={bidangBisnis.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={data.masterBidangBisnisId}
|
|
||||||
onChange={(value) => {
|
|
||||||
const isSameBidang = data.masterBidangBisnisId === value;
|
|
||||||
|
|
||||||
if (!isSameBidang) {
|
|
||||||
setListSubBidangSelected([{ id: "" }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setData({ ...(data as any), masterBidangBisnisId: value });
|
|
||||||
handlerSelectedSubBidang({ id: value as string });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{listSubBidangSelected.map((item, index) => (
|
|
||||||
<SelectCustom
|
|
||||||
key={index}
|
|
||||||
disabled={data.masterBidangBisnisId === ""}
|
|
||||||
label="Sub Bidang Usaha"
|
|
||||||
required
|
|
||||||
data={_.map(selectedSubBidang as any)
|
|
||||||
.filter((option: any) => {
|
|
||||||
const selectedValues = listSubBidangSelected.map((s) => s.id);
|
|
||||||
return (
|
|
||||||
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
|
|
||||||
!selectedValues.includes(option.id)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((e: any) => ({
|
|
||||||
value: e.id,
|
|
||||||
label: e.name,
|
|
||||||
}))}
|
|
||||||
value={item.id || null}
|
|
||||||
onChange={(value) => {
|
|
||||||
const list = _.clone(listSubBidangSelected);
|
|
||||||
list[index].id = value as any;
|
|
||||||
setListSubBidangSelected(list);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<CenterCustom>
|
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
||||||
<ActionIcon
|
|
||||||
disabled={
|
|
||||||
selectedSubBidang.length === listSubBidangSelected.length
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
setListSubBidangSelected([
|
|
||||||
...listSubBidangSelected,
|
|
||||||
{ id: "" },
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="add-circle-outline"
|
|
||||||
size={ICON_SIZE_XLARGE}
|
|
||||||
color={MainColor.black}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
<ActionIcon
|
|
||||||
disabled={listSubBidangSelected.length <= 1}
|
|
||||||
onPress={() => {
|
|
||||||
const list = _.clone(listSubBidangSelected);
|
|
||||||
list.pop();
|
|
||||||
setListSubBidangSelected(list);
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="remove-circle-outline"
|
|
||||||
size={ICON_SIZE_XLARGE}
|
|
||||||
color={MainColor.black}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</CenterCustom>
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
{/* <SelectCustom
|
|
||||||
label="Bidang Usaha"
|
|
||||||
required
|
|
||||||
data={bidangBisnis.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={null}
|
|
||||||
onChange={(value) => {
|
|
||||||
setData({ ...(data as any), masterBidangBisnisId: value });
|
|
||||||
handlerSelectedSubBidang({ id: value as string });
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* <ButtonCenteredOnly
|
|
||||||
onPress={() => {
|
|
||||||
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tambah Pilihan
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
<Spacing /> */}
|
|
||||||
|
|
||||||
{/* <TextCustom>{JSON.stringify(bidangBisnis, null, 2)}</TextCustom> */}
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
||||||
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
|
|
||||||
Nomor Telepon
|
|
||||||
</TextCustom>
|
|
||||||
<Text style={{ color: "red" }}> *</Text>
|
|
||||||
</View>
|
|
||||||
<Spacing height={5} />
|
|
||||||
<PhoneInput
|
|
||||||
value={inputValue}
|
|
||||||
onChangePhoneNumber={handleInputValue}
|
|
||||||
selectedCountry={selectedCountry}
|
|
||||||
onChangeSelectedCountry={handleSelectedCountry}
|
|
||||||
defaultCountry="ID"
|
|
||||||
placeholder="xxx-xxx-xxx"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<TextInputCustom
|
|
||||||
required
|
|
||||||
label="Alamat Bisnis"
|
|
||||||
placeholder="Masukkan alamat bisnis"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setData({ ...data, alamatKantor: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextAreaCustom
|
|
||||||
label="Deskripsi Bisnis"
|
|
||||||
placeholder="Masukkan deskripsi bisnis"
|
|
||||||
value={data.deskripsi}
|
|
||||||
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
|
|
||||||
autosize
|
|
||||||
minRows={2}
|
|
||||||
maxRows={5}
|
|
||||||
required
|
|
||||||
showCount
|
|
||||||
maxLength={1000}
|
|
||||||
/>
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
{/* Logo */}
|
|
||||||
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
|
|
||||||
|
|
||||||
<CenterCustom>
|
|
||||||
<Avatar.Image
|
|
||||||
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
|
|
||||||
size={200}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
<Spacing />
|
|
||||||
<ButtonCenteredOnly
|
|
||||||
icon="upload"
|
|
||||||
onPress={() => {
|
|
||||||
pickImage({
|
|
||||||
setImageUri,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</ButtonCenteredOnly>
|
|
||||||
<Spacing height={40} />
|
|
||||||
|
|
||||||
{/* Social Media */}
|
|
||||||
<InformationBox text="Isi hanya pada sosial media yang anda miliki." />
|
|
||||||
<TextInputCustom
|
|
||||||
label="Tiktok"
|
|
||||||
placeholder="Masukkan username tiktok"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setDataMedsos({ ...dataMedsos, tiktok: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInputCustom
|
|
||||||
label="Facebook"
|
|
||||||
placeholder="Masukkan username facebook"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setDataMedsos({ ...dataMedsos, facebook: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInputCustom
|
|
||||||
label="Instagram"
|
|
||||||
placeholder="Masukkan username instagram"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setDataMedsos({ ...dataMedsos, instagram: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInputCustom
|
|
||||||
label="Twitter"
|
|
||||||
placeholder="Masukkan username twitter"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setDataMedsos({ ...dataMedsos, twitter: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInputCustom
|
|
||||||
label="Youtube"
|
|
||||||
placeholder="Masukkan username youtube"
|
|
||||||
onChangeText={(value: any) =>
|
|
||||||
setDataMedsos({ ...dataMedsos, youtube: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/* <Spacing /> */}
|
|
||||||
</StackCustom>
|
|
||||||
</NewWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
@@ -49,7 +51,7 @@ export default function Portofolio() {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData(id as string);
|
onLoadData(id as string);
|
||||||
onLoadUserByToken();
|
onLoadUserByToken();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onLoadData(id: string) {
|
async function onLoadData(id: string) {
|
||||||
@@ -64,41 +66,52 @@ export default function Portofolio() {
|
|||||||
setProfileId(response?.data?.Profile?.id);
|
setProfileId(response?.data?.Profile?.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Portofolio",
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Portofolio"
|
||||||
data?.Profile?.id !== profileId ? null : (
|
left={<LeftButtonCustom />}
|
||||||
<TouchableOpacity onPress={openDrawer}>
|
right={
|
||||||
<Ionicons
|
data?.Profile?.id !== profileId ? null : (
|
||||||
name="ellipsis-vertical"
|
<TouchableOpacity onPress={openDrawer}>
|
||||||
size={20}
|
<Ionicons
|
||||||
color={MainColor.yellow}
|
name="ellipsis-vertical"
|
||||||
/>
|
size={20}
|
||||||
</TouchableOpacity>
|
color={MainColor.yellow}
|
||||||
),
|
/>
|
||||||
headerStyle: GStyles.headerStyle,
|
</TouchableOpacity>
|
||||||
headerTitleStyle: GStyles.headerTitleStyle,
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
{!data || !profileId ? (
|
{!data || !profileId ? (
|
||||||
<LoaderCustom />
|
<StackCustom>
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
<CustomSkeleton height={300} />
|
||||||
|
</StackCustom>
|
||||||
) : (
|
) : (
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<Portofolio_Data
|
<Portofolio_Data
|
||||||
data={data}
|
data={data}
|
||||||
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
|
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
|
||||||
/>
|
/>
|
||||||
<Portofolio_BusinessLocation
|
{data?.BusinessMaps && (
|
||||||
data={data?.BusinessMaps}
|
<Portofolio_BusinessLocation
|
||||||
imageId={data?.logoId}
|
data={data?.BusinessMaps}
|
||||||
setOpenDrawerLocation={setOpenDrawerLocation}
|
imageId={data?.logoId}
|
||||||
/>
|
setOpenDrawerLocation={setOpenDrawerLocation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Portofolio_SocialMediaSection
|
<Portofolio_SocialMediaSection
|
||||||
data={data?.Portofolio_MediaSosial}
|
data={data?.Portofolio_MediaSosial}
|
||||||
/>
|
/>
|
||||||
@@ -135,36 +148,38 @@ export default function Portofolio() {
|
|||||||
closeDrawer={() => setOpenDrawerLocation(false)}
|
closeDrawer={() => setOpenDrawerLocation(false)}
|
||||||
height={"auto"}
|
height={"auto"}
|
||||||
>
|
>
|
||||||
<DummyLandscapeImage
|
{data?.BusinessMaps?.imageId && (
|
||||||
height={200}
|
<DummyLandscapeImage
|
||||||
imageId={data?.BusinessMaps?.imageId}
|
height={200}
|
||||||
/>
|
imageId={data?.BusinessMaps?.imageId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="building-o"
|
name="building-o"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
rightItem={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="list-outline"
|
name="list-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={
|
rightItem={
|
||||||
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
|
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -172,26 +187,26 @@ export default function Portofolio() {
|
|||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="call-outline"
|
name="call-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
|
rightItem={<TextCustom>{data?.tlpn}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
rightItem={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacing />
|
<Spacing />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import { HeaderStyles } from "@/styles/header-styles";
|
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
export default function PortofolioLayout() {
|
export default function PortofolioLayout() {
|
||||||
@@ -7,8 +7,9 @@ export default function PortofolioLayout() {
|
|||||||
<>
|
<>
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
...HeaderStyles,
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader title="Portofolio" left={<LeftButtonCustom />} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <Stack.Screen name="[id]/index" options={{ title: "Portofolio" }} /> */}
|
{/* <Stack.Screen name="[id]/index" options={{ title: "Portofolio" }} /> */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { LoaderCustom } from "@/components";
|
import { NewWrapper, StackCustom } from "@/components";
|
||||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||||
import DrawerCustom from "@/components/Drawer/DrawerCustom";
|
import DrawerCustom from "@/components/Drawer/DrawerCustom";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -16,8 +17,8 @@ import { GStyles } from "@/styles/global-styles";
|
|||||||
import { IProfile } from "@/types/Type-Profile";
|
import { IProfile } from "@/types/Type-Profile";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import React, { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { TouchableOpacity } from "react-native";
|
import { RefreshControl, TouchableOpacity } from "react-native";
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
@@ -25,6 +26,7 @@ export default function Profile() {
|
|||||||
const [data, setData] = useState<IProfile>();
|
const [data, setData] = useState<IProfile>();
|
||||||
const [dataToken, setDataToken] = useState<IProfile>();
|
const [dataToken, setDataToken] = useState<IProfile>();
|
||||||
const [listPortofolio, setListPortofolio] = useState<any[]>();
|
const [listPortofolio, setListPortofolio] = useState<any[]>();
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const { token, logout, isAdmin, user, userData } = useAuth();
|
const { token, logout, isAdmin, user, userData } = useAuth();
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ export default function Profile() {
|
|||||||
onLoadUserByToken();
|
onLoadUserByToken();
|
||||||
isUserCheck();
|
isUserCheck();
|
||||||
userData(token as string);
|
userData(token as string);
|
||||||
}, [id, token])
|
}, [id, token]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isUserCheck = () => {
|
const isUserCheck = () => {
|
||||||
@@ -54,13 +56,21 @@ export default function Profile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onLoadData = async (id: string) => {
|
const onLoadData = async (id: string) => {
|
||||||
const response = await apiProfile({ id: id });
|
try {
|
||||||
setData(response.data);
|
const response = await apiProfile({ id: id });
|
||||||
|
setData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR onLoadData]", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoadUserByToken = async () => {
|
const onLoadUserByToken = async () => {
|
||||||
const response = await apiUser(user?.id as string);
|
try {
|
||||||
setDataToken(response?.data?.Profile);
|
const response = await apiUser(user?.id as string);
|
||||||
|
setDataToken(response?.data?.Profile);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[ERROR onLoadUserByToken]", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoadPortofolio = async (id: string) => {
|
const onLoadPortofolio = async (id: string) => {
|
||||||
@@ -69,37 +79,61 @@ export default function Profile() {
|
|||||||
const lastTwoByDate = response.data
|
const lastTwoByDate = response.data
|
||||||
.sort(
|
.sort(
|
||||||
(a: any, b: any) =>
|
(a: any, b: any) =>
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
) // urut desc
|
) // urut desc
|
||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
setListPortofolio(lastTwoByDate);
|
setListPortofolio(lastTwoByDate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ERROR]", error);
|
console.log("[ERROR onLoadPortofolio]", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefresh = useCallback(() => {
|
||||||
|
setRefreshing(true);
|
||||||
|
onLoadData(id as string);
|
||||||
|
onLoadPortofolio(id as string);
|
||||||
|
onLoadUserByToken();
|
||||||
|
isUserCheck();
|
||||||
|
userData(token as string);
|
||||||
|
setRefreshing(false);
|
||||||
|
}, [id, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Profile`,
|
header: () => (
|
||||||
headerLeft: () => <LeftButtonCustom />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Profile"
|
||||||
<ButtonnDot
|
left={<LeftButtonCustom />}
|
||||||
id={id as string}
|
right={
|
||||||
openDrawer={openDrawer}
|
<ButtonnDot
|
||||||
isUserCheck={isUserCheck()}
|
id={id as string}
|
||||||
logout={logout}
|
openDrawer={openDrawer}
|
||||||
|
isUserCheck={isUserCheck()}
|
||||||
|
logout={logout}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
headerStyle: GStyles.headerStyle,
|
|
||||||
headerTitleStyle: GStyles.headerTitleStyle,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Main View */}
|
{/* Main View */}
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
{!data || !dataToken ? (
|
{!data || !dataToken ? (
|
||||||
<LoaderCustom />
|
<StackCustom>
|
||||||
|
<CustomSkeleton height={400} />
|
||||||
|
<CustomSkeleton height={200} />
|
||||||
|
</StackCustom>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ProfileSection data={data as any} />
|
<ProfileSection data={data as any} />
|
||||||
@@ -110,7 +144,7 @@ export default function Profile() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
|
|
||||||
{/* Drawer Komponen Eksternal */}
|
{/* Drawer Komponen Eksternal */}
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
|
|||||||
@@ -1,47 +1,39 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
export default function ProfileLayout() {
|
export default function ProfileLayout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack
|
<Stack>
|
||||||
screenOptions={{
|
|
||||||
headerStyle: GStyles.headerStyle,
|
|
||||||
headerTitleStyle: GStyles.headerTitleStyle,
|
|
||||||
headerTitleAlign: "center",
|
|
||||||
headerBackButtonDisplayMode: "minimal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <Stack.Screen name="[id]/index" options={{ headerShown: false }} /> */}
|
{/* <Stack.Screen name="[id]/index" options={{ headerShown: false }} /> */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="[id]/edit"
|
name="[id]/edit"
|
||||||
options={{ title: "Edit Profile", headerLeft: () => <BackButton /> }}
|
options={{ header: () => <AppHeader title="Edit Profile" /> }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="[id]/update-photo"
|
name="[id]/update-photo"
|
||||||
options={{ title: "Update Foto", headerLeft: () => <BackButton /> }}
|
options={{ header: () => <AppHeader title="Update Foto" /> }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="[id]/update-background"
|
name="[id]/update-background"
|
||||||
options={{
|
options={{
|
||||||
title: "Update Latar Belakang",
|
header: () => <AppHeader title="Update Latar Belakang" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="create"
|
name="create"
|
||||||
options={{ title: "Buat Profile", headerBackVisible: false }}
|
options={{ headerBackVisible: false }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="[id]/blocked-list"
|
name="[id]/blocked-list"
|
||||||
options={{ title: "Daftar Blokir", headerLeft: () => <BackButton /> }}
|
options={{ header: () => <AppHeader title="Daftar Blokir" /> }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="[id]/detail-blocked"
|
name="[id]/detail-blocked"
|
||||||
options={{ title: "Detail Blokir", headerLeft: () => <BackButton /> }}
|
options={{ header: () => <AppHeader title="Detail Blokir" /> }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,36 +4,34 @@ import {
|
|||||||
IconHome,
|
IconHome,
|
||||||
IconStatus,
|
IconStatus,
|
||||||
} from "@/components/_Icon";
|
} from "@/components/_Icon";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||||
import { TabsStyles } from "@/styles/tabs-styles";
|
import { TabsStyles } from "@/styles/tabs-styles";
|
||||||
import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
|
import { router, Tabs, useLocalSearchParams } from "expo-router";
|
||||||
import { useLayoutEffect } from "react";
|
|
||||||
|
|
||||||
export default function VotingTabsLayout() {
|
export default function VotingTabsLayout() {
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const { from, category } = useLocalSearchParams<{
|
const { from, category } = useLocalSearchParams<{
|
||||||
from?: string;
|
from?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
console.log("from", from);
|
|
||||||
console.log("category", category);
|
|
||||||
|
|
||||||
// Atur header secara dinamis
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => (
|
|
||||||
<BackButtonFromNotification
|
|
||||||
from={from as string}
|
|
||||||
category={category as string}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}, [from, router, navigation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs screenOptions={TabsStyles}>
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
...TabsStyles,
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="Voting"
|
||||||
|
left={
|
||||||
|
<BackButtonFromNotification
|
||||||
|
from={from as string}
|
||||||
|
category={category as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
|
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
import ReportBox from "@/components/Box/ReportBox";
|
||||||
@@ -103,14 +104,19 @@ export default function VotingDetailStatus() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () =>
|
title="Detail"
|
||||||
status === "draft" ? (
|
left={<BackButton />}
|
||||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
right={
|
||||||
) : status === "publish" ? (
|
status === "draft" ? (
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||||
) : null,
|
) : status === "publish" ? (
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconContribution } from "@/components/_Icon";
|
import { IconContribution } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -81,10 +82,14 @@ export default function VotingDetailContribution() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Detail Kontribusi",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Detail Kontribusi"
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
left={<BackButton />}
|
||||||
|
right={
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Spacing,
|
Spacing,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconContribution } from "@/components/_Icon";
|
import { IconContribution } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -82,10 +83,14 @@ export default function VotingDetailHistory() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: "Riwayat Voting",
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Riwayat Voting"
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
left={<BackButton />}
|
||||||
|
right={
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { IconArchive, IconContribution } from "@/components/_Icon";
|
import { IconArchive, IconContribution } from "@/components/_Icon";
|
||||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
@@ -142,10 +143,14 @@ export default function VotingDetail() {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
title: `Detail Voting`,
|
header: () => (
|
||||||
headerLeft: () => <BackButton />,
|
<AppHeader
|
||||||
headerRight: () => (
|
title="Detail Voting"
|
||||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
left={<BackButton />}
|
||||||
|
right={
|
||||||
|
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
|
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
|
||||||
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
||||||
import { NotificationProvider } from "@/hooks/use-notification-store";
|
import { NotificationProvider } from "@/hooks/use-notification-store";
|
||||||
import { HeaderStyles } from "@/styles/header-styles";
|
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
export default function ApplicationLayout() {
|
export default function ApplicationLayout() {
|
||||||
@@ -20,7 +20,7 @@ export default function ApplicationLayout() {
|
|||||||
function ApplicationStack() {
|
function ApplicationStack() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack screenOptions={HeaderStyles}>
|
<Stack>
|
||||||
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
||||||
|
|
||||||
@@ -28,8 +28,7 @@ function ApplicationStack() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(image)/take-picture/[id]/index"
|
name="(image)/take-picture/[id]/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Ambil Gambar",
|
header: () => <AppHeader title="Ambil Gambar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -37,8 +36,7 @@ function ApplicationStack() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(image)/preview-image/[id]/index"
|
name="(image)/preview-image/[id]/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Preview Gambar",
|
header: () => <AppHeader title="Preview Gambar" />,
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
||||||
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
||||||
|
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
|
||||||
|
import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
|
||||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
import {
|
import {
|
||||||
ICON_SIZE_MEDIUM,
|
ICON_SIZE_MEDIUM,
|
||||||
@@ -15,11 +18,16 @@ import {
|
|||||||
ICON_SIZE_XLARGE,
|
ICON_SIZE_XLARGE,
|
||||||
} from "@/constants/constans-value";
|
} from "@/constants/constans-value";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
|
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
|
||||||
import {
|
import {
|
||||||
adminListMenu,
|
adminListMenu,
|
||||||
superAdminListMenu,
|
superAdminListMenu,
|
||||||
} from "@/screens/Admin/listPageAdmin";
|
} from "@/screens/Admin/listPageAdmin";
|
||||||
|
import {
|
||||||
|
adminListMenu_V2,
|
||||||
|
superAdminListMenu_V2,
|
||||||
|
} from "@/screens/Admin/listPageAdmin_V2";
|
||||||
import { GStyles } from "@/styles/global-styles";
|
import { GStyles } from "@/styles/global-styles";
|
||||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
@@ -28,12 +36,28 @@ import { useState } from "react";
|
|||||||
export default function AdminLayout() {
|
export default function AdminLayout() {
|
||||||
const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false);
|
const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false);
|
||||||
const [openDrawerUser, setOpenDrawerUser] = useState(false);
|
const [openDrawerUser, setOpenDrawerUser] = useState(false);
|
||||||
// const [user, setUser] = useState(null);
|
|
||||||
|
|
||||||
const { logout, user } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
|
|
||||||
console.log("[USER LAYOUT]", JSON.stringify(user, null, 2));
|
console.log("[USER LAYOUT]", JSON.stringify(user, null, 2));
|
||||||
|
|
||||||
|
const headerLeft = () => (
|
||||||
|
<Ionicons
|
||||||
|
name="menu"
|
||||||
|
size={ICON_SIZE_XLARGE}
|
||||||
|
color={MainColor.white}
|
||||||
|
onPress={() => setOpenDrawerNavbar(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerRight = () => (
|
||||||
|
<FontAwesome6
|
||||||
|
name="circle-user"
|
||||||
|
size={ICON_SIZE_MEDIUM}
|
||||||
|
color={MainColor.white}
|
||||||
|
onPress={() => setOpenDrawerUser(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -45,20 +69,33 @@ export default function AdminLayout() {
|
|||||||
contentStyle: {
|
contentStyle: {
|
||||||
borderBottomColor: AccentColor.blue,
|
borderBottomColor: AccentColor.blue,
|
||||||
},
|
},
|
||||||
headerLeft: () => (
|
|
||||||
<Ionicons
|
// headerLeft: () => (
|
||||||
name="menu"
|
// <Ionicons
|
||||||
size={ICON_SIZE_XLARGE}
|
// name="menu"
|
||||||
color={MainColor.white}
|
// size={ICON_SIZE_XLARGE}
|
||||||
onPress={() => setOpenDrawerNavbar(true)}
|
// color={MainColor.white}
|
||||||
/>
|
// onPress={() => setOpenDrawerNavbar(true)}
|
||||||
),
|
// />
|
||||||
headerRight: () => (
|
// ),
|
||||||
<FontAwesome6
|
// headerRight: () => (
|
||||||
name="circle-user"
|
// <FontAwesome6
|
||||||
size={ICON_SIZE_MEDIUM}
|
// name="circle-user"
|
||||||
color={MainColor.white}
|
// size={ICON_SIZE_MEDIUM}
|
||||||
onPress={() => setOpenDrawerUser(true)}
|
// color={MainColor.white}
|
||||||
|
// onPress={() => setOpenDrawerUser(true)}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
header: () => (
|
||||||
|
<AppHeader
|
||||||
|
title="HIPMI DASHBOARD"
|
||||||
|
showBack={false}
|
||||||
|
left={headerLeft()}
|
||||||
|
right={headerRight()}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -148,6 +185,24 @@ export default function AdminLayout() {
|
|||||||
}
|
}
|
||||||
onClose={() => setOpenDrawerNavbar(false)}
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V2
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* <NavbarMenu_V3
|
||||||
|
items={
|
||||||
|
user?.masterUserRoleId === "2"
|
||||||
|
? adminListMenu_V2
|
||||||
|
: superAdminListMenu_V2
|
||||||
|
}
|
||||||
|
onClose={() => setOpenDrawerNavbar(false)}
|
||||||
|
/> */}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</DrawerAdmin>
|
</DrawerAdmin>
|
||||||
|
|
||||||
@@ -198,7 +253,7 @@ export default function AdminLayout() {
|
|||||||
// size={ICON_SIZE_SMALL}
|
// size={ICON_SIZE_SMALL}
|
||||||
// color={MainColor.white}
|
// color={MainColor.white}
|
||||||
// />
|
// />
|
||||||
<AdminNotificationBell/>
|
<AdminNotificationBell />
|
||||||
),
|
),
|
||||||
path: "/admin/notification",
|
path: "/admin/notification",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenBusinessFieldDetail } from "@/screens/Admin/App-Information/ScreenBusinessFieldDetail";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
BaseBox,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenBusinessFieldDetail />;
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadDetail();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadDetail = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiAdminMasterBusinessFieldById({
|
|
||||||
id: id as string,
|
|
||||||
category: "all",
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
setData(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setData(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper>
|
|
||||||
<StackCustom>
|
|
||||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
|
||||||
|
|
||||||
{!data ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : (
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom bold>Nama Bidang</TextCustom>
|
|
||||||
<Spacing height={5} />
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<TextCustom bold>
|
|
||||||
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</TextCustom>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
span1={10}
|
|
||||||
span2={2}
|
|
||||||
text1={
|
|
||||||
<TextCustom bold size={"large"}>
|
|
||||||
{data?.bidang?.name}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={<IconEdit size={16} color={MainColor.black} />}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${id}/bidang-update`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
{/* <Divider /> */}
|
|
||||||
<Spacing height={5} />
|
|
||||||
|
|
||||||
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
|
|
||||||
<Spacing height={5} />
|
|
||||||
|
|
||||||
{data?.subBidang?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<StackCustom gap={0}>
|
|
||||||
<TextCustom bold>
|
|
||||||
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</TextCustom>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
span1={10}
|
|
||||||
span2={2}
|
|
||||||
text1={
|
|
||||||
<TextCustom bold size={"large"}>
|
|
||||||
{item.name}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconEdit size={16} color={MainColor.black} />
|
|
||||||
}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,5 @@
|
|||||||
import { ScrollableCustom, StackCustom, ViewWrapper } from "@/components";
|
import { Admin_ScreenAppInformation } from "@/screens/Admin/App-Information/ScreenAppInformation";
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
|
||||||
import AdminAppInformation_Bank from "@/screens/Admin/App-Information/InformationBankSection";
|
|
||||||
import AdminAppInformation_StickerSection from "@/screens/Admin/App-Information/StickerSection";
|
|
||||||
import { router } from "expo-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Alert } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminInformation() {
|
export default function AdminInformation() {
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
return <Admin_ScreenAppInformation />;
|
||||||
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
|
||||||
setActiveCategory(item.value);
|
|
||||||
setActivePage(item.label);
|
|
||||||
// tambahkan logika lain seperti filter dsb.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollComponent = (
|
|
||||||
<StackCustom>
|
|
||||||
<ScrollableCustom
|
|
||||||
data={listPage}
|
|
||||||
onButtonPress={handlePress}
|
|
||||||
activeId={activeCategory as any}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (activeCategory) {
|
|
||||||
case "bank":
|
|
||||||
return <AdminAppInformation_Bank />;
|
|
||||||
case "business":
|
|
||||||
return <AdminAppInformation_BusinessFieldSection />;
|
|
||||||
case "sticker":
|
|
||||||
return <AdminAppInformation_StickerSection />;
|
|
||||||
default:
|
|
||||||
return <AdminAppInformation_Bank />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={scrollComponent}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={activePage}
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
if (activeCategory === "bank") {
|
|
||||||
router.push("/admin/app-information/information-bank/create");
|
|
||||||
} else if (activeCategory === "business") {
|
|
||||||
router.push("/admin/app-information/business-field/create");
|
|
||||||
} else if (activeCategory === "sticker") {
|
|
||||||
Alert.alert("Coming Soon", "Next Update");
|
|
||||||
// router.push("/admin/app-information/sticker/create");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{renderContent()}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const listPage = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
label: "Informasi Bank",
|
|
||||||
value: "bank",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
label: "Bidang & Sub Bidang",
|
|
||||||
value: "business",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
label: "Stiker",
|
|
||||||
value: "sticker",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
BaseBox,
|
BaseBox,
|
||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
Grid,
|
Grid,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
@@ -22,7 +22,7 @@ export default function AdminCollaborationPublish() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
handlerLoadData();
|
handlerLoadData();
|
||||||
}, [status])
|
}, [status]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlerLoadData = async () => {
|
const handlerLoadData = async () => {
|
||||||
@@ -78,16 +78,16 @@ export default function AdminCollaborationPublish() {
|
|||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
{data?.report && (
|
{data?.report && (
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>Catatan report</TextCustom>}
|
leftItem={<TextCustom bold>Catatan report</TextCustom>}
|
||||||
rightIcon={<TextCustom>{data?.report}</TextCustom>}
|
rightItem={<TextCustom>{data?.report}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
)}
|
)}
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,19 +42,19 @@ export default function AdminDonationDetailDisbursementOfFunds() {
|
|||||||
const listData = [
|
const listData = [
|
||||||
{
|
{
|
||||||
label: "Nominal",
|
label: "Nominal",
|
||||||
value: `Rp ${(data && formatCurrencyDisplay(data?.nominalCair)) || 0}`,
|
value: `Rp ${data ? formatCurrencyDisplay(data?.nominalCair) : 0}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tanggal",
|
label: "Tanggal",
|
||||||
value: dateTimeView({ date: data?.createdAt }),
|
value: data ? dateTimeView({ date: data?.createdAt }) : "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Judul",
|
label: "Judul",
|
||||||
value: (data && data?.title) || "-",
|
value: data ? data?.title : "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Deskripsi",
|
label: "Deskripsi",
|
||||||
value: (data && data?.deskripsi) || "-",
|
value: data ? data?.deskripsi : "-",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,126 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationListDisbursementOfFunds } from "@/screens/Admin/Donation/ScreenDonationListDisbursementOfFunds";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
CenterCustom,
|
|
||||||
Divider,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminDonasiListOfDisbursementOfFunds() {
|
export default function AdminDonasiListOfDisbursementOfFunds() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenDonationListDisbursementOfFunds />;
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationDisbursementOfFundsListById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle title="Daftar Pencairan Dana" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Tanggal
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Nominal
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/donation/${item?.id}/detail-disbursement-of-funds`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom align="center" truncate>
|
|
||||||
{dayjs(item?.createdAt).format("DD-MM-YYYY")}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom align="center" truncate>
|
|
||||||
Rp. {formatCurrencyDisplay(item?.nominalCair)}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,186 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationListOfDonatur } from "@/screens/Admin/Donation/ScreenDonationListOfDonatur";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SelectCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { apiMasterTransaction } from "@/service/api-client/api-master";
|
|
||||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonasiListOfDonatur() {
|
export default function AdminDonasiListOfDonatur() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenDonationListOfDonatur />;
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
const [master, setMaster] = React.useState<any[]>([]);
|
|
||||||
|
|
||||||
const [selectValue, setSelectValue] = React.useState<string | null>(null);
|
|
||||||
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id, selectValue])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonationListOfDonatur({
|
|
||||||
id: id as string,
|
|
||||||
status: selectedStatus as any,
|
|
||||||
});
|
|
||||||
// console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoadMaster();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onLoadMaster = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMasterTransaction();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setMaster(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setMaster([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchComponent = (
|
|
||||||
<View style={{ flexDirection: "row", gap: 5 }}>
|
|
||||||
<SelectCustom
|
|
||||||
placeholder="Pilih status transaksi"
|
|
||||||
data={
|
|
||||||
_.isEmpty(master)
|
|
||||||
? []
|
|
||||||
: master?.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
value={selectValue}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
setSelectValue(value);
|
|
||||||
const nameSelected = master.find((item: any) => item.id === value);
|
|
||||||
const statusChooses = _.lowerCase(nameSelected?.name);
|
|
||||||
setSelectedStatus(statusChooses);
|
|
||||||
}}
|
|
||||||
styleContainer={{ width: "100%", marginBottom: 0 }}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle newComponent={searchComponent} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Donatur
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Status
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/donation/${item?.id}/${_.lowerCase(
|
|
||||||
item?.DonasiMaster_StatusInvoice?.name
|
|
||||||
)}/transaction-detail`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center" truncate>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "center" }}
|
|
||||||
color={colorBadgeTransaction({
|
|
||||||
status: item?.DonasiMaster_StatusInvoice?.name,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item?.DonasiMaster_StatusInvoice?.name}
|
|
||||||
</BadgeCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenDonationStatus } from "@/screens/Admin/Donation/ScreenDonationStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonationStatus() {
|
export default function AdminDonationStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenDonationStatus />;
|
||||||
console.log("[STATUS]", status);
|
|
||||||
|
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
const [loadData, setLoadData] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminDonation({
|
|
||||||
category: status as "publish" | "review" | "reject",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[RES]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={(value) => setSearch(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Donasi" />}>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Donasi"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(data) ? (
|
|
||||||
<TextCustom align="center" size="small" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
data?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export default function AdminDonationCategoryUpdate() {
|
|||||||
const response = await apiAdminMasterDonationCategoryById({
|
const response = await apiAdminMasterDonationCategoryById({
|
||||||
id: id as any,
|
id: id as any,
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
};
|
};
|
||||||
@@ -44,10 +43,9 @@ export default function AdminDonationCategoryUpdate() {
|
|||||||
id: id as any,
|
id: id as any,
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
router.back();
|
router.back();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log("Error update category:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
import {
|
import { Admin_ScreenDonationCategory } from "@/screens/Admin/Donation/ScreenDonationCategory";
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
ClickableCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { RefreshControl, View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminDonationCategory() {
|
export default function AdminDonationCategory() {
|
||||||
const [listData, setListData] = useState<any[]>([]);
|
return <Admin_ScreenDonationCategory />;
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
fetchMaster();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchMaster = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await apiAdminMasterDonationCategory();
|
|
||||||
if (response.success) {
|
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
setListData(response.data);
|
|
||||||
} else {
|
|
||||||
setListData([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[Error]", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRefresh = async () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
await fetchMaster();
|
|
||||||
setRefreshing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
||||||
}
|
|
||||||
headerComponent={<AdminTitlePage title="Donasi" />}
|
|
||||||
>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Kategori"
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/category-create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Status</TextCustom>}
|
|
||||||
value={<TextCustom bold>Kategori</TextCustom>}
|
|
||||||
/>
|
|
||||||
{/* <Grid>
|
|
||||||
<Grid.Col style={{ paddingLeft: 10 }} span={4}>
|
|
||||||
<TextCustom bold>Status</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>Kategori</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid> */}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{listData.map((item, index) => (
|
|
||||||
<ClickableCustom
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/donation/category-update?id=${item.id}`);
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={colorActivationForBadge({
|
|
||||||
status: item.active,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
value={<TextCustom>{item.name}</TextCustom>}
|
|
||||||
/>
|
|
||||||
{/* <Grid containerStyle={{ paddingBottom: 10 }}>
|
|
||||||
<Grid.Col span={4} style={{ paddingLeft: 10 }}>
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={item.active ? MainColor.green : MainColor.red}
|
|
||||||
>
|
|
||||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextCustom bold>{item.name}</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid> */}
|
|
||||||
<Divider />
|
|
||||||
</ClickableCustom>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
</View>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,254 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenEventDetail } from "@/screens/Admin/Event/ScreenEventDetail";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
AlertDefaultSystem,
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconDot, IconList } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
|
||||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
|
|
||||||
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
|
|
||||||
import { DEEP_LINK_URL } from "@/service/api-config";
|
|
||||||
import { colorBadgeStatus } from "@/utils/colorBadge";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import QRCode from "react-native-qrcode-svg";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function AdminEventDetail() {
|
export default function AdminEventDetail() {
|
||||||
const { user } = useAuth();
|
return <Admin_ScreenEventDetail />;
|
||||||
const { id, status } = useLocalSearchParams();
|
|
||||||
const [openDrawer, setOpenDrawer] = React.useState(false);
|
|
||||||
|
|
||||||
const [data, setData] = React.useState<any | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
|
|
||||||
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
|
|
||||||
|
|
||||||
const isDevLink =
|
|
||||||
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminEventById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const listData = [
|
|
||||||
{
|
|
||||||
label: "Pembuat Event",
|
|
||||||
value: (data && data?.Author?.username) || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Judul Event",
|
|
||||||
value: (data && data?.title) || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Status",
|
|
||||||
value:
|
|
||||||
(data && (
|
|
||||||
<BadgeCustom color={colorBadgeStatus({ status: status as string })}>
|
|
||||||
{_.startCase(status as string)}
|
|
||||||
</BadgeCustom>
|
|
||||||
)) ||
|
|
||||||
"-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Lokasi",
|
|
||||||
value: (data && data?.lokasi) || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Tipe Acara",
|
|
||||||
value: (data && data?.EventMaster_TipeAcara?.name) || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Mulai Event",
|
|
||||||
value:
|
|
||||||
(data && data?.tanggal && dateTimeView({ date: data?.tanggal })) || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Event Berakhir",
|
|
||||||
value:
|
|
||||||
(data &&
|
|
||||||
data?.tanggalSelesai &&
|
|
||||||
dateTimeView({ date: data?.tanggalSelesai })) ||
|
|
||||||
"-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Deskripsi",
|
|
||||||
value: (data && data?.deskripsi) || "-",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<ActionIcon
|
|
||||||
icon={<IconDot size={ICON_SIZE_BUTTON} />}
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawer(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlerSubmit = async () => {
|
|
||||||
try {
|
|
||||||
const response = await funUpdateStatusEvent({
|
|
||||||
id: id as string,
|
|
||||||
changeStatus: "publish",
|
|
||||||
data: { catatan: "", senderId: user?.id as string },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Gagal mempublikasikan event",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Event berhasil dipublikasikan",
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle
|
|
||||||
title={`Detail Data`}
|
|
||||||
rightComponent={
|
|
||||||
(status === "publish" || status === "history") && rightComponent
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom>
|
|
||||||
{listData.map((item, i) => (
|
|
||||||
<GridSpan_4_8
|
|
||||||
key={i}
|
|
||||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
|
||||||
value={<TextCustom>{item.value}</TextCustom>}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
|
|
||||||
<Spacing />
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
{data &&
|
|
||||||
data?.catatan &&
|
|
||||||
(status === "reject" || status === "review") && (
|
|
||||||
<ReportBox text={data?.catatan} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(status === "publish" || status === "history") && (
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom style={{ alignItems: "center" }}>
|
|
||||||
<TextCustom bold>QR Code Event</TextCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : (
|
|
||||||
<QRCode
|
|
||||||
value={isDevLink}
|
|
||||||
size={200}
|
|
||||||
// logo={require("@/assets/images/logo-hipmi.png")}
|
|
||||||
// logoSize={70}
|
|
||||||
// logoBackgroundColor="transparent"
|
|
||||||
// logoBorderRadius={50}
|
|
||||||
// color="black"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextCustom align="center">{isDevLink}</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status === "review" && (
|
|
||||||
<AdminButtonReview
|
|
||||||
onPublish={() => {
|
|
||||||
AlertDefaultSystem({
|
|
||||||
title: "Publish",
|
|
||||||
message: "Apakah anda yakin ingin mempublikasikan data ini?",
|
|
||||||
textLeft: "Batal",
|
|
||||||
textRight: "Ya",
|
|
||||||
onPressRight: () => handlerSubmit(),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onReject={() => {
|
|
||||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status === "reject" && (
|
|
||||||
<AdminButtonReject
|
|
||||||
title="Tambah Catatan"
|
|
||||||
onReject={() => {
|
|
||||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Spacing />
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: "Daftar Peserta",
|
|
||||||
icon: <IconList />,
|
|
||||||
path: `/admin/event/${id}/list-of-participants`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
setOpenDrawer(false);
|
|
||||||
router.push(item.path as any);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
Grid,
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { View } from "moti";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function AdminEventListOfParticipants() {
|
export default function AdminEventListOfParticipants() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenEventListOfParticipants />;
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
|
||||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminEventListOfParticipants({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
setStartDate(dayjs(response.data.Event.tanggal));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
|
||||||
>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada peserta
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<BaseBox key={index}>
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
{item?.User?.username}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
|
||||||
{startDate &&
|
|
||||||
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "flex-end" }}
|
|
||||||
color={item?.isPresent ? "green" : "red"}
|
|
||||||
>
|
|
||||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
|
||||||
</BadgeCustom>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "flex-end" }}
|
|
||||||
color="gray"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</BadgeCustom>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</BaseBox>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
ClickableCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminEventStatus() {
|
export default function AdminEventStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenEventStatus />;
|
||||||
console.log("[STATUS EVENT]", status);
|
|
||||||
|
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminEvent({
|
|
||||||
category: status as "publish" | "review" | "reject" | "history" as any,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[RES LIST BY STATUS: ${status}]`,
|
|
||||||
JSON.stringify(response, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={(value) => setSearch(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Username"
|
|
||||||
title2="Tanggal"
|
|
||||||
title3="Judul Event"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" size="small" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<ClickableCustom
|
|
||||||
key={index}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
// <ActionIcon
|
|
||||||
// icon={
|
|
||||||
// <Octicons
|
|
||||||
// name="eye"
|
|
||||||
// size={ICON_SIZE_BUTTON}
|
|
||||||
// color="black"
|
|
||||||
// />
|
|
||||||
// }
|
|
||||||
// onPress={() => {
|
|
||||||
// router.push(`/admin/event/${item.id}/${status}`);
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{dateTimeView({ date: item?.tanggal })}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider/>
|
|
||||||
</ClickableCustom>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,5 @@
|
|||||||
import {
|
import { Admin_ScreenEventTypeOfEvent } from "@/screens/Admin/Voting/ScreenEventTypeOfEvent";
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import { IconEdit } from "@/components/_Icon";
|
|
||||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
|
|
||||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminEventTypeOfEvent() {
|
export default function AdminEventTypeOfEvent() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenEventTypeOfEvent />;
|
||||||
const [loadData, setLoadData] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminMasterTypeOfEvent();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]",error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Tipe Acara"
|
|
||||||
rightComponent={
|
|
||||||
<AdminActionIconPlus
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/type-create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={<TextCustom bold align="center">Status</TextCustom>}
|
|
||||||
component3={<TextCustom bold>Tipe Acara</TextCustom>}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item, index) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconEdit size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/event/type-update?id=${item.id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
style2={{ alignItems: "center" }}
|
|
||||||
component2={
|
|
||||||
<CenterCustom>
|
|
||||||
<BadgeCustom
|
|
||||||
color={colorActivationForBadge({
|
|
||||||
status: item?.active,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item?.active ? "Aktif" : "Tidak Aktif"}
|
|
||||||
</BadgeCustom>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component3={<TextCustom >{item.name}</TextCustom>}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function AdminForumDetailPosting() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -72,6 +72,10 @@ export default function AdminForumDetailPosting() {
|
|||||||
label: "Total Report",
|
label: "Total Report",
|
||||||
value: data?.JumlahReportPosting || 0,
|
value: data?.JumlahReportPosting || 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Postingan",
|
||||||
|
value: (data && data?.diskusi) || "-",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -111,13 +115,6 @@ export default function AdminForumDetailPosting() {
|
|||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<TextCustom bold>Postingan</TextCustom>
|
|
||||||
<TextCustom>{(data && data?.diskusi) || "-"}</TextCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
|
|||||||
@@ -1,91 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumListComment } from "@/screens/Admin/Forum/ScreenForumListComment";
|
||||||
import {
|
|
||||||
LoaderCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import { IconOpenTo } from "@/components/_Icon/IconOpenTo";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import { apiAdminForumCommentById } from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminForumListComment() {
|
export default function AdminForumListComment() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenForumListComment />;
|
||||||
const [listComment, setListComment] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadComment();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadComment = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminForumCommentById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-all",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListComment(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListComment([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Komentar" />}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<AdminTitleTable title1="Aksi" title2="Report" title3="Komentar" />
|
|
||||||
<Divider />
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listComment) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada komentar
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listComment?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<IconOpenTo
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/forum/${item.id}/list-report-comment`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.countReport || 0}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>{item?.komentar || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,262 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumDetailReportComment } from "@/screens/Admin/Forum/ScreenForumDetailReportComment";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
AlertDefaultSystem,
|
|
||||||
BaseBox,
|
|
||||||
CenterCustom,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import {
|
|
||||||
apiAdminForumCommentById,
|
|
||||||
apiAdminForumDeactivateComment,
|
|
||||||
apiAdminForumListReportCommentById,
|
|
||||||
} from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function AdminForumReportComment() {
|
export default function AdminForumReportComment() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenForumDetailReportComment />;
|
||||||
const { user } = useAuth();
|
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
|
||||||
const [selectedReport, setSelectedReport] = useState({
|
|
||||||
id: "",
|
|
||||||
username: "",
|
|
||||||
kategori: "",
|
|
||||||
keterangan: "",
|
|
||||||
deskripsi: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminForumCommentById({
|
|
||||||
id: id as string,
|
|
||||||
category: "get-one",
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseReport = await apiAdminForumListReportCommentById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
if (responseReport.success) {
|
|
||||||
setListReport(responseReport.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setData(null);
|
|
||||||
setListReport([]);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle
|
|
||||||
title="Report Komentar"
|
|
||||||
rightComponent={
|
|
||||||
<ActionIcon
|
|
||||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
|
||||||
onPress={() => setOpenDrawer(true)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold>Username</TextCustom>}
|
|
||||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold>Komentar</TextCustom>}
|
|
||||||
text2={<TextCustom>{data?.komentar || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<AdminComp_BoxTitle title="Daftar Report Komentar" />
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
|
||||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listReport) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada report
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listReport?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawerAction(true);
|
|
||||||
setSelectedReport({
|
|
||||||
id: item.id,
|
|
||||||
username: item.User?.username,
|
|
||||||
kategori: item.ForumMaster_KategoriReport?.title,
|
|
||||||
keterangan:
|
|
||||||
item.ForumMaster_KategoriReport?.deskripsi,
|
|
||||||
deskripsi: item.deskripsi,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.User?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconTrash />,
|
|
||||||
label: "Hapus Komentar",
|
|
||||||
value: "delete",
|
|
||||||
path: "",
|
|
||||||
color: MainColor.red,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
AlertDefaultSystem({
|
|
||||||
title: "Hapus Komentar",
|
|
||||||
message: "Apakah Anda yakin ingin menghapus komentar ini?",
|
|
||||||
textLeft: "Batal",
|
|
||||||
textRight: "Hapus",
|
|
||||||
onPressRight: async () => {
|
|
||||||
const deleteComment = await apiAdminForumDeactivateComment({
|
|
||||||
id: id as string,
|
|
||||||
data: {
|
|
||||||
senderId: user?.id as string,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// if (!deleteComment.success) {
|
|
||||||
// Toast.show({
|
|
||||||
// type: "error",
|
|
||||||
// text1: "Komentar gagal dihapus",
|
|
||||||
// });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
setOpenDrawer(false);
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Komentar berhasil dihapus",
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawerAction}
|
|
||||||
closeDrawer={() => setOpenDrawerAction(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
|
||||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedReport?.kategori && (
|
|
||||||
<>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedReport?.deskripsi && (
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,283 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumDetailReportPosting } from "@/screens/Admin/Forum/ScreenForumDetailReportPosting";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
AlertDefaultSystem,
|
|
||||||
BadgeCustom,
|
|
||||||
BaseBox,
|
|
||||||
CenterCustom,
|
|
||||||
DrawerCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
MenuDrawerDynamicGrid,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import {
|
|
||||||
apiAdminForumDeactivatePosting,
|
|
||||||
apiAdminForumListReportPostingById,
|
|
||||||
apiAdminForumPostingById,
|
|
||||||
} from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export default function AdminForumReportPosting() {
|
export default function AdminForumDetailReportPosting() {
|
||||||
const { user } = useAuth();
|
return <Admin_ScreenForumDetailReportPosting />;
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
|
||||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
|
||||||
|
|
||||||
const [data, setData] = useState<any | null>(null);
|
|
||||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
|
||||||
const [loadListReport, setLoadListReport] = useState(false);
|
|
||||||
const [selectedReport, setSelectedReport] = useState({
|
|
||||||
id: "",
|
|
||||||
username: "",
|
|
||||||
kategori: "",
|
|
||||||
keterangan: "",
|
|
||||||
deskripsi: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadListReport(true);
|
|
||||||
const response = await apiAdminForumPostingById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseReport = await apiAdminForumListReportPostingById({
|
|
||||||
id: id as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseReport.success) {
|
|
||||||
setListReport(responseReport.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadListReport(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminBackButtonAntTitle
|
|
||||||
title="Report Posting"
|
|
||||||
rightComponent={
|
|
||||||
<ActionIcon
|
|
||||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
|
||||||
onPress={() => setOpenDrawerPage(true)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BaseBox>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold>Username</TextCustom>}
|
|
||||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold>Status</TextCustom>}
|
|
||||||
text2={
|
|
||||||
data && data?.ForumMaster_StatusPosting?.status ? (
|
|
||||||
<BadgeCustom
|
|
||||||
color={
|
|
||||||
data?.ForumMaster_StatusPosting?.status === "Open"
|
|
||||||
? MainColor.green
|
|
||||||
: MainColor.red
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data?.ForumMaster_StatusPosting?.status === "Open"
|
|
||||||
? "Open"
|
|
||||||
: "Close"}
|
|
||||||
</BadgeCustom>
|
|
||||||
) : (
|
|
||||||
<TextCustom>{"-"}</TextCustom>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold>Postingan</TextCustom>}
|
|
||||||
text2={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
</StackCustom>
|
|
||||||
</BaseBox>
|
|
||||||
|
|
||||||
<AdminComp_BoxTitle title="Daftar Report Posting" />
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
|
||||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
{loadListReport ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listReport) ? (
|
|
||||||
<TextCustom align="center" color={"gray"}>
|
|
||||||
Belum ada report
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listReport?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawerAction(true);
|
|
||||||
setSelectedReport({
|
|
||||||
id: item?.id,
|
|
||||||
username: item?.User?.username,
|
|
||||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
|
||||||
keterangan:
|
|
||||||
item?.ForumMaster_KategoriReport?.deskripsi,
|
|
||||||
deskripsi: item?.deskripsi,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom truncate>
|
|
||||||
{item?.User?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawerPage}
|
|
||||||
closeDrawer={() => setOpenDrawerPage(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<MenuDrawerDynamicGrid
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
icon: <IconTrash />,
|
|
||||||
label: "Hapus Posting",
|
|
||||||
value: "delete",
|
|
||||||
path: "",
|
|
||||||
color: MainColor.red,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPressItem={(item) => {
|
|
||||||
AlertDefaultSystem({
|
|
||||||
title: "Hapus Posting",
|
|
||||||
message: "Apakah Anda yakin ingin menghapus posting ini?",
|
|
||||||
textLeft: "Batal",
|
|
||||||
textRight: "Hapus",
|
|
||||||
onPressRight: async () => {
|
|
||||||
const response = await apiAdminForumDeactivatePosting({
|
|
||||||
id: id as string,
|
|
||||||
data: {
|
|
||||||
senderId: user?.id as string,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Posting gagal dihapus",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpenDrawerPage(false);
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Posting berhasil dihapus",
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawerAction}
|
|
||||||
closeDrawer={() => setOpenDrawerAction(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
|
||||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedReport?.kategori && (
|
|
||||||
<>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedReport?.deskripsi && (
|
|
||||||
<GridSpan_4_8
|
|
||||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
|
||||||
value={
|
|
||||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumPosting } from "@/screens/Admin/Forum/ScreenForumPosting";
|
||||||
import {
|
|
||||||
ClickableCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminForumPosting() {
|
export default function AdminForumPosting() {
|
||||||
const [list, setList] = useState<any | null>(null);
|
return <Admin_ScreenForumPosting />;
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
handlerLoadList();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminForum({
|
|
||||||
category: "posting",
|
|
||||||
search: search,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("DATA", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari postingan"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
|
||||||
<AdminComp_BoxTitle title={"Posting"} rightComponent={rightComponent} />
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={<TextCustom bold truncate>Username</TextCustom>}
|
|
||||||
text2={<TextCustom bold truncate> Postingan</TextCustom>}
|
|
||||||
text3={<TextCustom bold align="center" truncate> Report Posting</TextCustom>}
|
|
||||||
text4={<TextCustom bold align="center" truncate> Komentar</TextCustom>}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
<StackCustom>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<ClickableCustom
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/forum/${item.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom truncate>
|
|
||||||
{item?.diskusi || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text3={
|
|
||||||
<TextCustom align="center" truncate={2}>
|
|
||||||
{item?.reportPosting || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text4={
|
|
||||||
<TextCustom align="center" truncate={2}>
|
|
||||||
{item?.komentar || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</ClickableCustom>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumReportComment } from "@/screens/Admin/Forum/ScreenForumReportComment";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
ClickableCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminForumReportComment() {
|
export default function AdminForumReportComment() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenForumReportComment />;
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
|
|
||||||
const response = await apiAdminForum({
|
|
||||||
category: "report_comment",
|
|
||||||
search: search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari Komentar"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Report Komentar"
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Pelapor
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Komentar
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text3={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Jenis Laporan
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<Spacing />
|
|
||||||
<StackCustom gap={"lg"}>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<ClickableCustom
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.User?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.Forum_Komentar?.komentar || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ClickableCustom>
|
|
||||||
<Spacing />
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenForumReportPosting } from "@/screens/Admin/Forum/ScreenForumReportPosting";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
ClickableCustom,
|
|
||||||
Divider,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminForumReportPosting() {
|
export default function AdminForumReportPosting() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenForumReportPosting />;
|
||||||
const [loadList, setLoadList] = useState<boolean>(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
|
|
||||||
const response = await apiAdminForum({
|
|
||||||
category: "report_posting",
|
|
||||||
search: search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari Postingan"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Report Posting"
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Pelapor
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
Postingan
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<ClickableCustom
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GridSpan_NewComponent
|
|
||||||
text1={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.User?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
text2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Forum_Posting?.diskusi || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ClickableCustom>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
|
|||||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
import ReportBox from "@/components/Box/ReportBox";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
@@ -182,9 +183,9 @@ export default function AdminInvestmentDetail() {
|
|||||||
|
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<GridSpan_4_8
|
<GridTwoView
|
||||||
label={<TextCustom bold>File Prospektus</TextCustom>}
|
leftItem={<TextCustom bold>File Prospektus</TextCustom>}
|
||||||
value={
|
rightItem={
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
iconLeft={
|
iconLeft={
|
||||||
<IconProspectus
|
<IconProspectus
|
||||||
@@ -202,9 +203,10 @@ export default function AdminInvestmentDetail() {
|
|||||||
</ButtonCustom>
|
</ButtonCustom>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<GridSpan_4_8
|
<GridTwoView
|
||||||
label={<TextCustom bold>File Dokumen</TextCustom>}
|
|
||||||
value={
|
leftItem={<TextCustom bold>File Dokumen</TextCustom>}
|
||||||
|
rightItem={
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
{_.isEmpty(data?.DokumenInvestasi) ? (
|
{_.isEmpty(data?.DokumenInvestasi) ? (
|
||||||
<TextCustom align="center">-</TextCustom>
|
<TextCustom align="center">-</TextCustom>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
AlertDefaultSystem,
|
AlertDefaultSystem,
|
||||||
BadgeCustom,
|
BadgeCustom,
|
||||||
BaseBox,
|
BaseBox,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||||
@@ -15,8 +15,8 @@ import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
|||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
apiAdminInvestmentGetOneInvoiceById,
|
apiAdminInvestmentGetOneInvoiceById,
|
||||||
apiAdminInvestmentUpdateInvoice,
|
apiAdminInvestmentUpdateInvoice,
|
||||||
} from "@/service/api-admin/api-admin-investment";
|
} from "@/service/api-admin/api-admin-investment";
|
||||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
||||||
import { dateTimeView } from "@/utils/dateTimeView";
|
import { dateTimeView } from "@/utils/dateTimeView";
|
||||||
@@ -60,7 +60,7 @@ export default function AdminInvestmentTransactionDetail() {
|
|||||||
value: (data && data?.MasterBank?.namaBank) || "-",
|
value: (data && data?.MasterBank?.namaBank) || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Jumlah Investasi",
|
label: "Nominal",
|
||||||
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
|
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,7 +158,7 @@ export default function AdminInvestmentTransactionDetail() {
|
|||||||
spanRight={6}
|
spanRight={6}
|
||||||
styleLeft={{ paddingRight: 10 }}
|
styleLeft={{ paddingRight: 10 }}
|
||||||
styleRight={{ paddingLeft: 10 }}
|
styleRight={{ paddingLeft: 10 }}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -181,7 +181,7 @@ export default function AdminInvestmentTransactionDetail() {
|
|||||||
Tolak
|
Tolak
|
||||||
</ButtonCustom>
|
</ButtonCustom>
|
||||||
}
|
}
|
||||||
rightIcon={
|
rightItem={
|
||||||
<ButtonCustom
|
<ButtonCustom
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -1,195 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenInvestmentListOfInvestor } from "@/screens/Admin/Investment/ScreenInvestmentListOfInvestor";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
LoaderCustom,
|
|
||||||
SelectCustom,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import { IconView } from "@/components/_Icon/IconComponent";
|
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminInvestmentListOfInvestor } from "@/service/api-admin/api-admin-investment";
|
|
||||||
import { apiMasterTransaction } from "@/service/api-client/api-master";
|
|
||||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminInvestmentListOfInvestor() {
|
export default function AdminInvestmentListOfInvestor() {
|
||||||
const { id } = useLocalSearchParams();
|
return <Admin_ScreenInvestmentListOfInvestor />;
|
||||||
console.log("[ID]", id);
|
|
||||||
|
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadData] = React.useState(false);
|
|
||||||
const [master, setMaster] = React.useState<any[]>([]);
|
|
||||||
|
|
||||||
const [selectValue, setSelectValue] = React.useState<string | null>(null);
|
|
||||||
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoadMaster();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onLoadMaster = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiMasterTransaction();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setMaster(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setMaster([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [id, selectValue])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadData(true);
|
|
||||||
const response = await apiAdminInvestmentListOfInvestor({
|
|
||||||
id: id as string,
|
|
||||||
status: selectedStatus as any,
|
|
||||||
});
|
|
||||||
console.log("[LIST OF INVESTOR]", JSON.stringify(response, null, 2));
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoadMaster();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const searchComponent = (
|
|
||||||
<View style={{ flexDirection: "row", gap: 5 }}>
|
|
||||||
<SelectCustom
|
|
||||||
placeholder="Pilih status transaksi"
|
|
||||||
data={
|
|
||||||
_.isEmpty(master)
|
|
||||||
? []
|
|
||||||
: master?.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
value={selectValue}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
setSelectValue(value);
|
|
||||||
const nameSelected = master.find((item: any) => item.id === value);
|
|
||||||
const statusChooses = _.lowerCase(nameSelected?.name);
|
|
||||||
setSelectedStatus(statusChooses);
|
|
||||||
}}
|
|
||||||
styleContainer={{ width: "100%", marginBottom: 0 }}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
const headerComponent = (
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<AdminBackButtonAntTitle title="Daftar Investor" />
|
|
||||||
{searchComponent}
|
|
||||||
</StackCustom>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={headerComponent}>
|
|
||||||
<StackCustom>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Investor
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<TextCustom bold align="center">
|
|
||||||
Status
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<StackCustom>
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<View key={index}>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={3}
|
|
||||||
span2={5}
|
|
||||||
span3={4}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(
|
|
||||||
`/admin/investment/${item?.id}/${_.lowerCase(
|
|
||||||
item?.StatusInvoice?.name
|
|
||||||
)}/transaction-detail`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold align="center" truncate>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<BadgeCustom
|
|
||||||
style={{ alignSelf: "center" }}
|
|
||||||
color={colorBadgeTransaction({
|
|
||||||
status: item?.StatusInvoice?.name,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item?.StatusInvoice?.name}
|
|
||||||
</BadgeCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenInvestmentStatus } from "@/screens/Admin/Investment/ScreenInvestmentStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminInvestment } from "@/service/api-admin/api-admin-investment";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminInvestmentStatus() {
|
export default function AdminInvestmentStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenInvestmentStatus />;
|
||||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
|
||||||
const [loadData, setLoadingData] = React.useState(false);
|
|
||||||
const [search, setSearch] = React.useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadingData(true);
|
|
||||||
const response = await apiAdminInvestment({
|
|
||||||
category: status as "publish" | "review" | "reject",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
setListData([]);
|
|
||||||
} finally {
|
|
||||||
setLoadingData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Investasi" />}>
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Investasi"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadData ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<NoDataText />
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/investment/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={<TextCustom truncate={1}>{item?.author?.username}</TextCustom>}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
BaseBox,
|
BaseBox,
|
||||||
DummyLandscapeImage,
|
DummyLandscapeImage,
|
||||||
Grid,
|
Grid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
@@ -120,7 +121,7 @@ export default function AdminJobDetailStatus() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||||
>
|
>
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
@@ -184,7 +185,7 @@ export default function AdminJobDetailStatus() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
AlertDefaultSystem,
|
AlertDefaultSystem,
|
||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
|
NewWrapper,
|
||||||
TextAreaCustom,
|
TextAreaCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
@@ -100,7 +101,7 @@ export default function AdminJobRejectInput() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
footerComponent={buttonSubmit}
|
footerComponent={buttonSubmit}
|
||||||
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
||||||
>
|
>
|
||||||
@@ -112,7 +113,7 @@ export default function AdminJobRejectInput() {
|
|||||||
showCount
|
showCount
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
/>
|
/>
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminJobStatus() {
|
export default function AdminJobStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenJobStatus />;
|
||||||
const [list, setList] = useState<any | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
handlerLoadList();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminJob({
|
|
||||||
category: status as "publish" | "review" | "reject",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
placeholder="Cari"
|
|
||||||
onChangeText={setSearch}
|
|
||||||
value={search}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Job Vacancy" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Pekerjaan"
|
|
||||||
/>
|
|
||||||
{/* <Spacing /> */}
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list?.map((item: any, index: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={index}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/job/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom align="center" truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2} align="center">
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
import { ButtonCustom, DrawerCustom, DummyLandscapeImage, Grid, Spacing, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
import {
|
||||||
|
ButtonCustom,
|
||||||
|
DrawerCustom,
|
||||||
|
DummyLandscapeImage,
|
||||||
|
Grid,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
ViewWrapper,
|
||||||
|
} from "@/components";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
||||||
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
||||||
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { View } from "react-native";
|
|
||||||
import MapView, { Marker } from "react-native-maps";
|
|
||||||
|
|
||||||
const defaultRegion = {
|
|
||||||
latitude: -8.737109,
|
|
||||||
longitude: 115.1756897,
|
|
||||||
latitudeDelta: 0.1,
|
|
||||||
longitudeDelta: 0.1,
|
|
||||||
height: 300,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface LocationItem {
|
|
||||||
id: string | number;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
name: string;
|
|
||||||
imageId?: string;
|
|
||||||
}
|
|
||||||
export default function AdminMaps() {
|
export default function AdminMaps() {
|
||||||
const [list, setList] = useState<any[] | null>(null);
|
const [list, setList] = useState<any[] | null>(null);
|
||||||
const [loadList, setLoadList] = useState(false);
|
const [loadList, setLoadList] = useState(false);
|
||||||
@@ -45,7 +36,7 @@ export default function AdminMaps() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
handlerLoadList();
|
handlerLoadList();
|
||||||
}, [])
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlerLoadList = async () => {
|
const handlerLoadList = async () => {
|
||||||
@@ -63,74 +54,30 @@ export default function AdminMaps() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const markers: MapMarker[] = list?.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
coordinate: [item.longitude, item.latitude] as [number, number],
|
||||||
|
imageId: item.Portofolio?.logoId,
|
||||||
|
onSelected: () => {
|
||||||
|
setOpenDrawer(true);
|
||||||
|
setSelected({
|
||||||
|
id: item?.id,
|
||||||
|
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
|
||||||
|
nomorTelepon: item?.Portofolio?.tlpn,
|
||||||
|
alamatBisnis: item?.Portofolio?.alamatKantor,
|
||||||
|
namePin: item?.namePin,
|
||||||
|
imageId: item?.imageId,
|
||||||
|
portofolioId: item?.Portofolio?.id,
|
||||||
|
latitude: item?.latitude,
|
||||||
|
longitude: item?.longitude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
|
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
|
||||||
{/* <MapCustom height={"100%"} /> */}
|
<MapsV2Custom markers={markers} />
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
{loadList ? (
|
|
||||||
<MapView
|
|
||||||
initialRegion={defaultRegion}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<MapView
|
|
||||||
initialRegion={defaultRegion}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{list?.map((item: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={item?.id}
|
|
||||||
coordinate={{
|
|
||||||
latitude: item?.latitude,
|
|
||||||
longitude: item?.longitude,
|
|
||||||
}}
|
|
||||||
title={item?.namePin}
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawer(true);
|
|
||||||
setSelected({
|
|
||||||
id: item?.id,
|
|
||||||
bidangBisnis:
|
|
||||||
item?.Portofolio?.MasterBidangBisnis?.name,
|
|
||||||
nomorTelepon: item?.Portofolio?.tlpn,
|
|
||||||
alamatBisnis: item?.Portofolio?.alamatKantor,
|
|
||||||
namePin: item?.namePin,
|
|
||||||
imageId: item?.imageId,
|
|
||||||
portofolioId: item?.Portofolio?.id,
|
|
||||||
latitude: item?.latitude,
|
|
||||||
longitude: item?.longitude,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
// Gunakan gambar kustom jika tersedia
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: API_IMAGE.GET({
|
|
||||||
fileId: item?.Portofolio?.logoId,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: 30,
|
|
||||||
height: 30,
|
|
||||||
borderRadius: 100,
|
|
||||||
borderWidth: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</MapView>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ViewWrapper>
|
</ViewWrapper>
|
||||||
|
|
||||||
<DrawerCustom
|
<DrawerCustom
|
||||||
@@ -138,58 +85,60 @@ export default function AdminMaps() {
|
|||||||
closeDrawer={() => setOpenDrawer(false)}
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
height={"auto"}
|
height={"auto"}
|
||||||
>
|
>
|
||||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
{selected.imageId && (
|
||||||
|
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||||
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="building-o"
|
name="building-o"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
|
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="list-outline"
|
name="list-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="call-outline"
|
name="call-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
rightItem={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
spanLeft={2}
|
||||||
spanRight={10}
|
spanRight={10}
|
||||||
leftIcon={
|
leftItem={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="location-outline"
|
name="location-outline"
|
||||||
size={ICON_SIZE_SMALL}
|
size={ICON_SIZE_SMALL}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import {
|
import {
|
||||||
apiAdminUserAccessGetById,
|
apiAdminUserAccessGetById,
|
||||||
apiAdminUserAccessUpdateStatus,
|
apiAdminUserAccessUpdateStatus,
|
||||||
} from "@/service/api-admin/api-admin-user-access";
|
} from "@/service/api-admin/api-admin-user-access";
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@@ -26,7 +26,7 @@ export default function SuperAdminDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -48,7 +48,7 @@ export default function SuperAdminDetail() {
|
|||||||
const response = await apiAdminUserAccessUpdateStatus({
|
const response = await apiAdminUserAccessUpdateStatus({
|
||||||
id: id as string,
|
id: id as string,
|
||||||
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
||||||
category: "role"
|
category: "role",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
@@ -102,8 +102,8 @@ export default function SuperAdminDetail() {
|
|||||||
key={index}
|
key={index}
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import {
|
||||||
BoxButtonOnFooter,
|
BoxButtonOnFooter,
|
||||||
ButtonCustom,
|
ButtonCustom,
|
||||||
LoaderCustom,
|
LoaderCustom,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
ViewWrapper,
|
ViewWrapper,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
apiAdminUserAccessGetById,
|
apiAdminUserAccessGetById,
|
||||||
apiAdminUserAccessUpdateStatus,
|
apiAdminUserAccessUpdateStatus,
|
||||||
} from "@/service/api-admin/api-admin-user-access";
|
} from "@/service/api-admin/api-admin-user-access";
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@@ -28,7 +28,7 @@ export default function AdminUserAccessDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -102,8 +102,8 @@ export default function AdminUserAccessDetail() {
|
|||||||
key={index}
|
key={index}
|
||||||
spanLeft={4}
|
spanLeft={4}
|
||||||
spanRight={8}
|
spanRight={8}
|
||||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
|
|||||||
@@ -1,139 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenUserAccess } from "@/screens/Admin/User-Access/ScreenUserAccess";
|
||||||
import {
|
|
||||||
BadgeCustom,
|
|
||||||
CenterCustom,
|
|
||||||
Divider,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
|
||||||
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
export default function AdminUserAccess() {
|
export default function AdminUserAccess() {
|
||||||
const [listData, setListData] = useState<any[] | null>(null);
|
return <Admin_ScreenUserAccess />;
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiAdminUserAccessGetAll({
|
|
||||||
search: search,
|
|
||||||
category: "only-user",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setListData(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR LOAD DATA]", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari User"
|
|
||||||
onChangeText={(text) => setSearch(text)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="User Access"
|
|
||||||
rightComponent={rightComponent()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GridViewCustomSpan
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<TextCustom align="center" bold>
|
|
||||||
Aksi
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component2={<TextCustom bold>Username</TextCustom>}
|
|
||||||
component3={
|
|
||||||
<TextCustom align="center" bold>
|
|
||||||
Status Akses
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<StackCustom>
|
|
||||||
{_.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray" size={"small"}>
|
|
||||||
Tidak ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((item: any, index: number) => (
|
|
||||||
<GridViewCustomSpan
|
|
||||||
key={index}
|
|
||||||
span1={2}
|
|
||||||
span2={5}
|
|
||||||
span3={5}
|
|
||||||
component1={
|
|
||||||
<CenterCustom>
|
|
||||||
<Ionicons
|
|
||||||
onPress={() =>
|
|
||||||
router.push(`/admin/user-access/${item?.id}`)
|
|
||||||
}
|
|
||||||
name="open"
|
|
||||||
size={ICON_SIZE_XLARGE}
|
|
||||||
color={MainColor.yellow}
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
// <ButtonCustom
|
|
||||||
// onPress={() =>
|
|
||||||
// router.push(`/admin/user-access/${item?.id}`)
|
|
||||||
// }
|
|
||||||
// >
|
|
||||||
// Detail
|
|
||||||
// </ButtonCustom>
|
|
||||||
}
|
|
||||||
component2={
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
{item?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
component3={
|
|
||||||
<CenterCustom>
|
|
||||||
{item?.active ? (
|
|
||||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
|
||||||
) : (
|
|
||||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
|
||||||
)}
|
|
||||||
</CenterCustom>
|
|
||||||
}
|
|
||||||
style3={{ alignItems: "center", justifyContent: "center" }}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
BaseBox,
|
BaseBox,
|
||||||
CircleContainer,
|
CircleContainer,
|
||||||
Grid,
|
Grid,
|
||||||
|
NewWrapper,
|
||||||
Spacing,
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
import ReportBox from "@/components/Box/ReportBox";
|
import ReportBox from "@/components/Box/ReportBox";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import { MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -40,7 +41,7 @@ export default function AdminVotingDetail() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
onLoadData();
|
onLoadData();
|
||||||
}, [id])
|
}, [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLoadData = async () => {
|
const onLoadData = async () => {
|
||||||
@@ -169,26 +170,28 @@ export default function AdminVotingDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
|
hideFooter
|
||||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||||
>
|
>
|
||||||
<BaseBox>
|
<BaseBox>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
{listData.map((item, i) => (
|
{listData.map((item, i) => (
|
||||||
<GridSpan_4_8
|
<GridTwoView
|
||||||
key={i}
|
key={i}
|
||||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
spanLeft={5}
|
||||||
value={<TextCustom>{item.value}</TextCustom>}
|
spanRight={7}
|
||||||
|
leftItem={<TextCustom bold>{item.label}</TextCustom>}
|
||||||
|
rightItem={<TextCustom>{item.value}</TextCustom>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
|
|
||||||
{status === "publish" ||
|
{(status === "publish" || status === "history") && (
|
||||||
(status === "history" && (
|
<BaseBox>
|
||||||
<BaseBox>
|
<TextCustom bold align="center">
|
||||||
<TextCustom bold align="center">
|
Hasil Voting
|
||||||
Hasil Voting
|
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -209,11 +212,11 @@ export default function AdminVotingDetail() {
|
|||||||
</TextCustom>
|
</TextCustom>
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
{data &&
|
{data &&
|
||||||
data?.catatan &&
|
data?.catatan &&
|
||||||
@@ -250,7 +253,7 @@ export default function AdminVotingDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Spacing />
|
<Spacing />
|
||||||
</ViewWrapper>
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenVotingStatus } from "@/screens/Admin/Voting/ScreenVotingStatus";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminVotingStatus() {
|
export default function AdminVotingStatus() {
|
||||||
const { status } = useLocalSearchParams();
|
return <Admin_ScreenVotingStatus />;
|
||||||
const [list, setList] = useState<any | null>(null);
|
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [status, search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminVoting({
|
|
||||||
category: status as "publish" | "review" | "reject" as any,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title={`${_.startCase(status as string)}`}
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Voting"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(list) ? (
|
|
||||||
<TextCustom align="center" bold color="gray">
|
|
||||||
Belum ada data
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
list.map((item: any, i: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={i}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/voting/${item.id}/${status}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={
|
|
||||||
<TextCustom truncate={1}>
|
|
||||||
{item?.Author?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
import { Admin_ScreenVotingHistory } from "@/screens/Admin/Voting/ScreenVotingHistory";
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
LoaderCustom,
|
|
||||||
SearchInput,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
ViewWrapper
|
|
||||||
} from "@/components";
|
|
||||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
|
||||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
|
||||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
|
||||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
|
||||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
|
||||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
|
||||||
import { Octicons } from "@expo/vector-icons";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Divider } from "react-native-paper";
|
|
||||||
|
|
||||||
export default function AdminVotingHistory() {
|
export default function AdminVotingHistory() {
|
||||||
const [list, setList] = useState<any | null>(null);
|
return <Admin_ScreenVotingHistory />;
|
||||||
const [loadList, setLoadList] = useState(false);
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
onLoadData();
|
|
||||||
}, [ search])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLoadData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadList(true);
|
|
||||||
const response = await apiAdminVoting({
|
|
||||||
category: "history",
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ERROR]", error);
|
|
||||||
} finally {
|
|
||||||
setLoadList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightComponent = (
|
|
||||||
<SearchInput
|
|
||||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
|
||||||
placeholder="Cari"
|
|
||||||
value={search}
|
|
||||||
onChangeText={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
|
||||||
<AdminComp_BoxTitle
|
|
||||||
title="Riwayat"
|
|
||||||
rightComponent={rightComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StackCustom gap={"sm"}>
|
|
||||||
<AdminTitleTable
|
|
||||||
title1="Aksi"
|
|
||||||
title2="Username"
|
|
||||||
title3="Judul Voting"
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{loadList ? <LoaderCustom/> : _.isEmpty(list) ? <TextCustom align="center" bold color="gray">Belum ada data</TextCustom> : list.map((item: any, i: number) => (
|
|
||||||
<AdminTableValue
|
|
||||||
key={i}
|
|
||||||
value1={
|
|
||||||
<ActionIcon
|
|
||||||
icon={
|
|
||||||
<Octicons
|
|
||||||
name="eye"
|
|
||||||
size={ICON_SIZE_BUTTON}
|
|
||||||
color="black"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/admin/voting/${item.id}/history`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
|
||||||
value3={
|
|
||||||
<TextCustom truncate={2}>
|
|
||||||
{item?.title || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||||
|
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
@@ -15,7 +16,7 @@ export default function NotFoundScreen() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
|
options={{ header: () => <AppHeader title="" left={<BackButton onPress={() => handleBack()} />} /> }}
|
||||||
/>
|
/>
|
||||||
<ViewWrapper>
|
<ViewWrapper>
|
||||||
<StackCustom
|
<StackCustom
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ export default function BackButtonFromNotification({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BackButton
|
<BackButton
|
||||||
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (from === "notifications") {
|
if (from === "notifications") {
|
||||||
router.replace(`/notifications?category=${category}`);
|
router.push(`/notifications?category=${category}`);
|
||||||
} else {
|
} else {
|
||||||
if (from) {
|
if (from) {
|
||||||
router.replace(`/${from}` as any);
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
router.navigate("/home");
|
router.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
276
components/Drawer/NavbarMenu.back.tsx
Normal file
276
components/Drawer/NavbarMenu.back.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Set activeLink saat pathname berubah
|
||||||
|
useEffect(() => {
|
||||||
|
if (normalizedPathname) {
|
||||||
|
setActiveLink(normalizedPathname);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
// flex: 1,
|
||||||
|
// backgroundColor: MainColor.black,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10, // Opsional: tambahkan padding
|
||||||
|
}}
|
||||||
|
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
activeLink={activeLink}
|
||||||
|
setActiveLink={setActiveLink}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
activeLink,
|
||||||
|
setActiveLink,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem;
|
||||||
|
onClose?: () => void;
|
||||||
|
activeLink: string | null;
|
||||||
|
setActiveLink: (link: string | null) => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const isActive = activeLink === item.link;
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
React.useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.parentText}>{item.label}</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
// {
|
||||||
|
// backgroundColor: "red",
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 40],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.links.map((subItem, index) => {
|
||||||
|
const isSubActive = activeLink === subItem.link;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, isSubActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(subItem.link);
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={isSubActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
isSubActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveLink(item.link || null);
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
// backgroundColor: AccentColor.darkblue,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -28,7 +28,7 @@ interface NavbarMenuProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [activeLink, setActiveLink] = useState<string | null>(null);
|
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||||
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (normalizedPathname) {
|
if (normalizedPathname) {
|
||||||
setActiveLink(normalizedPathname);
|
setActiveLink(normalizedPathname);
|
||||||
|
|
||||||
|
// Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
|
||||||
|
for (const item of items) {
|
||||||
|
// Cocokkan dengan link langsung
|
||||||
|
if (item.link && normalizedPathname.startsWith(item.link)) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cocokkan dengan submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
|
||||||
|
if (matchingSubItem) {
|
||||||
|
setOpenKeys(prev => {
|
||||||
|
if (!prev.includes(item.label)) {
|
||||||
|
return [...prev, item.label];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [normalizedPathname]);
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
// Toggle dropdown
|
// Toggle dropdown
|
||||||
const toggleOpen = (label: string) => {
|
const toggleOpen = (label: string) => {
|
||||||
setOpenKeys((prev) =>
|
setOpenKeys((prev) =>
|
||||||
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,35 +125,71 @@ function MenuItem({
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggleOpen: () => void;
|
toggleOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
// Cek apakah menu ini atau submenu-nya yang aktif
|
||||||
const isActive = activeLink === item.link;
|
const isActive = activeLink === item.link;
|
||||||
|
|
||||||
|
// Cek apakah path saat ini cocok dengan salah satu submenu
|
||||||
|
const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
|
||||||
|
|
||||||
|
// Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
|
||||||
|
const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return activeLink.startsWith(linkPath + "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
|
||||||
|
const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
|
||||||
|
|
||||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
// Animasi saat isOpen berubah
|
// Animasi saat isOpen berubah
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
|
||||||
|
const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
|
||||||
|
|
||||||
Animated.timing(animatedHeight, {
|
Animated.timing(animatedHeight, {
|
||||||
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||||
duration: 200,
|
duration: 200,
|
||||||
useNativeDriver: false,
|
useNativeDriver: false,
|
||||||
}).start();
|
}).start();
|
||||||
}, [isOpen, item.links, animatedHeight]);
|
|
||||||
|
// Jika perlu membuka dropdown otomatis, panggil toggleOpen
|
||||||
|
if (shouldAutoOpen) {
|
||||||
|
toggleOpen();
|
||||||
|
}
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
|
||||||
|
|
||||||
// Jika ada submenu
|
// Jika ada submenu
|
||||||
if (item.links && item.links.length > 0) {
|
if (item.links && item.links.length > 0) {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{/* Parent Item */}
|
{/* Parent Item */}
|
||||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isMenuActive && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.parentText}>{item.label}</Text>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isMenuActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
size={16}
|
size={16}
|
||||||
color={MainColor.white}
|
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
parentText: {
|
parentText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V2 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V2[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// Check jika ada submenu yang match dengan current path
|
||||||
|
const hasActiveSubmenu = item.links.some((subItem) => {
|
||||||
|
return checkPathMatch(subItem.link, subItem.detailPattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasActiveSubmenu) {
|
||||||
|
newOpenKeys.push(item.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V2;
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
console.log('🔍 Pattern Match Check:', {
|
||||||
|
currentPath,
|
||||||
|
detailPattern,
|
||||||
|
regex: patternRegex.toString(),
|
||||||
|
isMatch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
const hasActiveSubmenu =
|
||||||
|
item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
// PRE-CALCULATE semua active states untuk submenu
|
||||||
|
const submenuActiveStates = item.links.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
hasActiveSubmenu && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
hasActiveSubmenu && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
dashboardLink: subItem.link,
|
||||||
|
dashboardLength: pathLength,
|
||||||
|
otherLabel: other.subItem.label,
|
||||||
|
otherLink: other.subItem.link,
|
||||||
|
otherPattern: other.subItem.detailPattern,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
otherIsActive: other.isActive,
|
||||||
|
isOtherLonger,
|
||||||
|
willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
if (isSubActive && other.isActive) {
|
||||||
|
console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
current: subItem.label,
|
||||||
|
currentPath: subItem.link,
|
||||||
|
currentLength: pathLength,
|
||||||
|
other: other.subItem.label,
|
||||||
|
otherPath: other.subItem.link,
|
||||||
|
otherLength: other.pathLength,
|
||||||
|
isOtherLonger,
|
||||||
|
shouldDisableCurrent: isOtherLonger,
|
||||||
|
currentURL: currentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
if (isSubActive) {
|
||||||
|
console.log('✅ Active check:', {
|
||||||
|
label: subItem.label,
|
||||||
|
link: subItem.link,
|
||||||
|
isSubActive,
|
||||||
|
hasMoreSpecificMatch,
|
||||||
|
finalIsActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, finalIsActive && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={finalIsActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
finalIsActive && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router, usePathname } from "expo-router";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
export interface NavbarItem_V3 {
|
||||||
|
label: string;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
links?: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||||
|
}[];
|
||||||
|
initiallyOpened?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarMenuProps {
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Normalisasi path: hapus trailing slash
|
||||||
|
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||||
|
|
||||||
|
// Auto-open parent menu jika submenu aktif
|
||||||
|
useEffect(() => {
|
||||||
|
if (!normalizedPathname || !items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newOpenKeys: string[] = [];
|
||||||
|
|
||||||
|
// Helper function yang sama dengan di MenuItem
|
||||||
|
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedPathname === normalizedLink) return true;
|
||||||
|
|
||||||
|
// Detail pattern match
|
||||||
|
if (detailPattern) {
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
if (patternRegex.test(normalizedPathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail page match (fallback)
|
||||||
|
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate all potential matches for conflict resolution
|
||||||
|
const allMatches = items.flatMap(item => {
|
||||||
|
if (!item.links || item.links.length === 0) return [];
|
||||||
|
|
||||||
|
return item.links
|
||||||
|
.filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
|
||||||
|
.map(subItem => ({
|
||||||
|
parentLabel: item.label,
|
||||||
|
subItem,
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the most specific match for each parent
|
||||||
|
const uniqueParents = new Map<string, { parentLabel: string, longestPathLength: number }>();
|
||||||
|
|
||||||
|
allMatches.forEach(match => {
|
||||||
|
const existing = uniqueParents.get(match.parentLabel);
|
||||||
|
if (!existing || match.pathLength > existing.longestPathLength) {
|
||||||
|
uniqueParents.set(match.parentLabel, {
|
||||||
|
parentLabel: match.parentLabel,
|
||||||
|
longestPathLength: match.pathLength
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add only the parents with the most specific matches
|
||||||
|
newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
|
||||||
|
|
||||||
|
// Additionally, if no specific submenu match was found but the current path
|
||||||
|
// starts with one of the parent menu links, add that parent
|
||||||
|
if (newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
if (commonWords.includes(segment.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPageGlobal = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW: Only open parent menu if the current path is a detail page of the most relevant parent
|
||||||
|
if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
|
||||||
|
// Find the parent whose link is the longest prefix of the current path
|
||||||
|
let longestMatchParent = null;
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
item.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
longestMatchParent = item.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (longestMatchParent) {
|
||||||
|
newOpenKeys.push(longestMatchParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenKeys(newOpenKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in NavbarMenu useEffect:", error);
|
||||||
|
}
|
||||||
|
}, [normalizedPathname, items]);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleOpen = (label: string) => {
|
||||||
|
setOpenKeys((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items && items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
onClose={onClose}
|
||||||
|
currentPath={normalizedPathname}
|
||||||
|
isOpen={openKeys.includes(item.label)}
|
||||||
|
toggleOpen={() => toggleOpen(item.label)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen Item Menu
|
||||||
|
function MenuItem({
|
||||||
|
item,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
currentPath,
|
||||||
|
isOpen,
|
||||||
|
toggleOpen,
|
||||||
|
}: {
|
||||||
|
item: NavbarItem_V3;
|
||||||
|
items: NavbarItem_V3[];
|
||||||
|
onClose?: () => void;
|
||||||
|
currentPath: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
toggleOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Helper function untuk check apakah path aktif
|
||||||
|
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||||
|
if (!linkPath) return false;
|
||||||
|
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 1. Match exact - prioritas tertinggi
|
||||||
|
if (currentPath === normalizedLink) return true;
|
||||||
|
|
||||||
|
// 2. Jika ada detailPattern, cek pattern dulu
|
||||||
|
if (detailPattern) {
|
||||||
|
// detailPattern contoh: "/admin/job/*/review"
|
||||||
|
// akan match dengan:
|
||||||
|
// - /admin/job/123/review ✅
|
||||||
|
// - /admin/job/123/review/transaction-detail ✅
|
||||||
|
// - /admin/job/123/review/anything/nested ✅
|
||||||
|
const patternRegex = new RegExp(
|
||||||
|
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||||
|
);
|
||||||
|
const isMatch = patternRegex.test(currentPath);
|
||||||
|
|
||||||
|
// Debug log untuk pattern matching
|
||||||
|
// if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||||
|
// console.log('🔍 Pattern Match Check:', {
|
||||||
|
// currentPath,
|
||||||
|
// detailPattern,
|
||||||
|
// regex: patternRegex.toString(),
|
||||||
|
// isMatch
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Match untuk detail pages (fallback)
|
||||||
|
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||||
|
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||||
|
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasCommonWord = segments.some(segment =>
|
||||||
|
commonWords.includes(segment.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hanya anggap sebagai detail page jika mengandung commonWords
|
||||||
|
return hasCommonWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||||
|
const isActive = isPathActive(item.link);
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||||
|
const isOnDetailPage = (() => {
|
||||||
|
// Check if current path has ID-like segments or detail keywords
|
||||||
|
const segments = currentPath.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) return false;
|
||||||
|
|
||||||
|
const commonWords = [
|
||||||
|
// Actions
|
||||||
|
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||||
|
|
||||||
|
// Status types
|
||||||
|
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||||
|
|
||||||
|
// General pages
|
||||||
|
'category', 'history', 'dashboard', 'index',
|
||||||
|
|
||||||
|
// Event specific
|
||||||
|
'type-of-event', 'type-create', 'type-update',
|
||||||
|
|
||||||
|
// Forum specific
|
||||||
|
'posting', 'report-posting', 'report-comment',
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
'group',
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
'business-field', 'information-bank', 'sticker',
|
||||||
|
'bidang-update', 'sub-bidang-update',
|
||||||
|
|
||||||
|
// Transaction/Finance related
|
||||||
|
'transaction-detail', 'transaction', 'payment',
|
||||||
|
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||||
|
'list-disbursement-of-funds',
|
||||||
|
|
||||||
|
// List pages (CRITICAL!)
|
||||||
|
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||||
|
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||||
|
|
||||||
|
// Input/Form pages
|
||||||
|
'reject-input',
|
||||||
|
|
||||||
|
// Category pages
|
||||||
|
'category-create', 'category-update'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any segment is a common word
|
||||||
|
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||||
|
|
||||||
|
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||||
|
const hasIdSegment = segments.some(segment => {
|
||||||
|
const isPureNumber = /^\d+$/.test(segment);
|
||||||
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||||
|
const hasNumber = /\d/.test(segment);
|
||||||
|
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||||
|
|
||||||
|
return isPureNumber || isUUID || isAlphanumericId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A detail page is one that has either common words or ID segments
|
||||||
|
return hasCommonWord || hasIdSegment;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Calculate all submenu active states for conflict resolution
|
||||||
|
const submenuActiveStates = item.links?.map(subItem => ({
|
||||||
|
subItem,
|
||||||
|
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||||
|
pathLength: subItem.link.length
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Determine if any submenu is active considering conflicts
|
||||||
|
const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
|
||||||
|
if (!isSubActive) return false;
|
||||||
|
|
||||||
|
// Check if there's a more specific match elsewhere
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
return other.isActive && other.pathLength > pathLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isSubActive && !hasMoreSpecificMatch;
|
||||||
|
}) || false;
|
||||||
|
|
||||||
|
// For parent menu detection, if current path contains common words,
|
||||||
|
// check if this parent menu's link is a prefix of the current path
|
||||||
|
const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
|
||||||
|
|
||||||
|
// Determine if this is the most relevant parent menu for the current path
|
||||||
|
const isMostRelevantParent = isParentOfDetailPage && (() => {
|
||||||
|
let longestMatchLength = 0;
|
||||||
|
let mostRelevantParent = null;
|
||||||
|
|
||||||
|
// Find the parent with the longest matching prefix
|
||||||
|
items.forEach(parentItem => {
|
||||||
|
if (parentItem.links && parentItem.links.length > 0) {
|
||||||
|
parentItem.links.forEach(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||||
|
longestMatchLength = linkPath.length;
|
||||||
|
mostRelevantParent = parentItem.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mostRelevantParent === item.label;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
|
||||||
|
const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
|
||||||
|
|
||||||
|
// NEW LOGIC: If user is on a detail page that belongs to this parent menu,
|
||||||
|
// activate only the parent menu (open dropdown) without activating any submenu
|
||||||
|
const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
|
||||||
|
item.links && item.links.length > 0 &&
|
||||||
|
item.links.some(link => {
|
||||||
|
const linkPath = link.link.replace(/\/+$/, "");
|
||||||
|
return currentPath.startsWith(linkPath + "/");
|
||||||
|
}) &&
|
||||||
|
!isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
|
||||||
|
|
||||||
|
// NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
|
||||||
|
const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
|
||||||
|
|
||||||
|
// NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
|
||||||
|
// then it should not be highlighted even if it would normally be the most relevant
|
||||||
|
const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
|
||||||
|
|
||||||
|
// NEW LOGIC: If this is an unlisted page, no menu should be highlighted
|
||||||
|
const isUnlistedPageAndNotRelevant = isUnlistedPage;
|
||||||
|
|
||||||
|
// FINAL LOGIC: Only activate this menu if:
|
||||||
|
// 1. It's the exact match for current path, OR
|
||||||
|
// 2. It's the most relevant parent, OR
|
||||||
|
// 3. It's a detail page of this menu
|
||||||
|
// But NOT if we're on a detail page and this isn't the relevant parent
|
||||||
|
// And NOT if this is an unlisted page
|
||||||
|
const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
|
||||||
|
|
||||||
|
// Animasi saat isOpen berubah
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedHeight, {
|
||||||
|
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
|
||||||
|
|
||||||
|
// Jika ada submenu
|
||||||
|
if (item.links && item.links.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{/* Parent Item */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.parentItem,
|
||||||
|
isActuallyRelevant && styles.parentItemActive,
|
||||||
|
]}
|
||||||
|
onPress={toggleOpen}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.parentText,
|
||||||
|
isActuallyRelevant && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||||
|
size={16}
|
||||||
|
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Submenu (Animated) */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.submenu,
|
||||||
|
{
|
||||||
|
height: animatedHeight,
|
||||||
|
opacity: animatedHeight.interpolate({
|
||||||
|
inputRange: [0, item.links.length * 44],
|
||||||
|
outputRange: [0, 1],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||||
|
|
||||||
|
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||||
|
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||||
|
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||||
|
|
||||||
|
const isOtherLonger = other.pathLength > pathLength;
|
||||||
|
|
||||||
|
// Debug log untuk Dashboard
|
||||||
|
// if (subItem.label === "Dashboard" && isSubActive) {
|
||||||
|
// console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||||
|
// dashboardLink: subItem.link,
|
||||||
|
// dashboardLength: pathLength,
|
||||||
|
// otherLabel: other.subItem.label,
|
||||||
|
// otherLink: other.subItem.link,
|
||||||
|
// otherPattern: other.subItem.detailPattern,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// otherIsActive: other.isActive,
|
||||||
|
// isOtherLonger,
|
||||||
|
// willDisableDashboard: other.isActive && isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Conflict log
|
||||||
|
// if (isSubActive && other.isActive) {
|
||||||
|
// console.log('🔍 CONFLICT DETECTED:', {
|
||||||
|
// current: subItem.label,
|
||||||
|
// currentPath: subItem.link,
|
||||||
|
// currentLength: pathLength,
|
||||||
|
// other: other.subItem.label,
|
||||||
|
// otherPath: other.subItem.link,
|
||||||
|
// otherLength: other.pathLength,
|
||||||
|
// isOtherLonger,
|
||||||
|
// shouldDisableCurrent: isOtherLonger,
|
||||||
|
// currentURL: currentPath
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return other.isActive && isOtherLonger;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||||
|
|
||||||
|
// NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
|
||||||
|
// Also don't highlight if this is an unlisted page
|
||||||
|
const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
|
||||||
|
|
||||||
|
// Debug final
|
||||||
|
// if (isSubActive) {
|
||||||
|
// console.log('✅ Active check:', {
|
||||||
|
// label: subItem.label,
|
||||||
|
// link: subItem.link,
|
||||||
|
// isSubActive,
|
||||||
|
// hasMoreSpecificMatch,
|
||||||
|
// finalIsActive,
|
||||||
|
// shouldHighlight,
|
||||||
|
// isOnDetailPage,
|
||||||
|
// isUnlistedPage
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(subItem.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="radio-button-on-outline"
|
||||||
|
size={16}
|
||||||
|
color={shouldHighlight ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.subText,
|
||||||
|
shouldHighlight && { color: MainColor.yellow },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{subItem.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu tanpa submenu
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||||
|
onPress={() => {
|
||||||
|
onClose?.();
|
||||||
|
router.push(item.link as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? MainColor.yellow : MainColor.white}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.singleText,
|
||||||
|
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
parentItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
parentItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
parentText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
singleItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
singleItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
singleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
marginLeft: 10,
|
||||||
|
color: MainColor.white,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
overflow: "hidden",
|
||||||
|
marginLeft: 30,
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
subItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
subItemActive: {
|
||||||
|
backgroundColor: AccentColor.blue,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
color: MainColor.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
110
components/Map/MapSelectedPlatform.tsx
Normal file
110
components/Map/MapSelectedPlatform.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
|
import MapSelected from "./MapSelected";
|
||||||
|
import MapSelectedV2 from "./MapSelectedV2";
|
||||||
|
import { LatLng } from "react-native-maps";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props untuk komponen MapSelectedPlatform
|
||||||
|
* Mendukung kedua format koordinat (LatLng untuk iOS, [number, number] untuk Android)
|
||||||
|
*/
|
||||||
|
export interface MapSelectedPlatformProps {
|
||||||
|
/** Region awal kamera */
|
||||||
|
initialRegion?: {
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
latitudeDelta?: number;
|
||||||
|
longitudeDelta?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Lokasi yang dipilih (support kedua format) */
|
||||||
|
selectedLocation: LatLng | [number, number] | null;
|
||||||
|
|
||||||
|
/** Callback ketika lokasi dipilih */
|
||||||
|
onLocationSelect: (location: LatLng | [number, number]) => void;
|
||||||
|
|
||||||
|
/** Tinggi peta dalam pixels (default: 400) */
|
||||||
|
height?: number;
|
||||||
|
|
||||||
|
/** Tampilkan lokasi user (default: true) */
|
||||||
|
showUserLocation?: boolean;
|
||||||
|
|
||||||
|
/** Tampilkan tombol my location (default: true) */
|
||||||
|
showsMyLocationButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen Map yang otomatis memilih implementasi berdasarkan platform
|
||||||
|
*
|
||||||
|
* Platform Strategy:
|
||||||
|
* - **iOS**: Menggunakan react-native-maps (MapSelected)
|
||||||
|
* - **Android**: Menggunakan @maplibre/maplibre-react-native (MapSelectedV2)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <MapSelectedPlatform
|
||||||
|
* selectedLocation={selectedLocation}
|
||||||
|
* onLocationSelect={setSelectedLocation}
|
||||||
|
* height={300}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function MapSelectedPlatform({
|
||||||
|
initialRegion,
|
||||||
|
selectedLocation,
|
||||||
|
onLocationSelect,
|
||||||
|
height = 400,
|
||||||
|
showUserLocation = true,
|
||||||
|
showsMyLocationButton = true,
|
||||||
|
}: MapSelectedPlatformProps) {
|
||||||
|
// iOS: Gunakan react-native-maps
|
||||||
|
// if (Platform.OS === "ios") {
|
||||||
|
// return (
|
||||||
|
// <MapSelected
|
||||||
|
// initialRegion={initialRegion}
|
||||||
|
// selectedLocation={(selectedLocation as LatLng) || { latitude: 0, longitude: 0 }}
|
||||||
|
// setSelectedLocation={(location: LatLng) => {
|
||||||
|
// onLocationSelect(location);
|
||||||
|
// }}
|
||||||
|
// height={height}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Android: Gunakan MapLibre
|
||||||
|
// Konversi dari LatLng ke [longitude, latitude] jika perlu
|
||||||
|
const androidLocation: [number, number] | undefined = selectedLocation
|
||||||
|
? isLatLng(selectedLocation)
|
||||||
|
? [selectedLocation.longitude, selectedLocation.latitude]
|
||||||
|
: selectedLocation
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapSelectedV2
|
||||||
|
selectedLocation={androidLocation}
|
||||||
|
onLocationSelect={(location: [number, number]) => {
|
||||||
|
// Konversi dari [longitude, latitude] ke LatLng untuk konsistensi
|
||||||
|
const latLng: LatLng = {
|
||||||
|
latitude: location[1],
|
||||||
|
longitude: location[0],
|
||||||
|
};
|
||||||
|
onLocationSelect(latLng);
|
||||||
|
}}
|
||||||
|
height={height}
|
||||||
|
// showUserLocation={showUserLocation}
|
||||||
|
// showsMyLocationButton={showsMyLocationButton}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard untuk mengecek apakah object adalah LatLng
|
||||||
|
*/
|
||||||
|
function isLatLng(location: any): location is LatLng {
|
||||||
|
return (
|
||||||
|
location &&
|
||||||
|
typeof location.latitude === "number" &&
|
||||||
|
typeof location.longitude === "number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapSelectedPlatform;
|
||||||
150
components/Map/MapSelectedV2.tsx
Normal file
150
components/Map/MapSelectedV2.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||||
|
import { StyleSheet, View, ActivityIndicator } from "react-native";
|
||||||
|
import {
|
||||||
|
MapView,
|
||||||
|
Camera,
|
||||||
|
PointAnnotation,
|
||||||
|
} from "@maplibre/maplibre-react-native";
|
||||||
|
import * as Location from "expo-location";
|
||||||
|
|
||||||
|
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
const DEBOUNCE_MS = 800;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedLocation?: [number, number];
|
||||||
|
onLocationSelect?: (location: [number, number]) => void;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MapSelectedV2({
|
||||||
|
selectedLocation,
|
||||||
|
onLocationSelect,
|
||||||
|
height = 400,
|
||||||
|
}: Props) {
|
||||||
|
const lastTapRef = useRef<number>(0);
|
||||||
|
const cameraRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const [userLocation, setUserLocation] = useState<[number, number] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isLoadingLocation, setIsLoadingLocation] = useState(true);
|
||||||
|
|
||||||
|
// ✅ Ambil lokasi user saat pertama mount
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
console.log("Permission lokasi ditolak");
|
||||||
|
setIsLoadingLocation(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = await Location.getCurrentPositionAsync({
|
||||||
|
accuracy: Location.Accuracy.Balanced,
|
||||||
|
});
|
||||||
|
|
||||||
|
const coords: [number, number] = [
|
||||||
|
location.coords.longitude,
|
||||||
|
location.coords.latitude,
|
||||||
|
];
|
||||||
|
|
||||||
|
setUserLocation(coords);
|
||||||
|
|
||||||
|
// ✅ Fly ke posisi user jika belum ada selectedLocation
|
||||||
|
if (!selectedLocation && cameraRef.current) {
|
||||||
|
cameraRef.current.flyTo(coords, 1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Gagal ambil lokasi:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingLocation(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isLoadingLocation]);
|
||||||
|
|
||||||
|
const handleMapPress = useCallback(
|
||||||
|
(event: any) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastTapRef.current < DEBOUNCE_MS) return;
|
||||||
|
lastTapRef.current = now;
|
||||||
|
|
||||||
|
const coords = event?.geometry?.coordinates;
|
||||||
|
if (!coords || coords.length < 2) return;
|
||||||
|
|
||||||
|
onLocationSelect?.([coords[0], coords[1]]);
|
||||||
|
},
|
||||||
|
[onLocationSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Center awal kamera:
|
||||||
|
// 1. Jika ada selectedLocation → pakai itu
|
||||||
|
// 2. Jika ada userLocation → pakai itu
|
||||||
|
// 3. Fallback → Bali
|
||||||
|
const initialCenter: [number, number] = selectedLocation ??
|
||||||
|
userLocation ?? [115.1756897, -8.737109];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ height, width: "100%" }}>
|
||||||
|
{/* Loading indicator saat fetch lokasi */}
|
||||||
|
{isLoadingLocation && (
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<ActivityIndicator size="small" color="#0a1f44" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MapView
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
mapStyle={MAP_STYLE}
|
||||||
|
onPress={handleMapPress}
|
||||||
|
logoEnabled={false}
|
||||||
|
>
|
||||||
|
<Camera
|
||||||
|
ref={cameraRef}
|
||||||
|
defaultSettings={{
|
||||||
|
centerCoordinate: initialCenter,
|
||||||
|
zoomLevel: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedLocation && (
|
||||||
|
<PointAnnotation
|
||||||
|
id="selected-location"
|
||||||
|
key="selected-location"
|
||||||
|
coordinate={selectedLocation}
|
||||||
|
>
|
||||||
|
<View style={styles.dot} />
|
||||||
|
</PointAnnotation>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
dot: {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "#0a1f44",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#fff",
|
||||||
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 10,
|
||||||
|
alignSelf: "center",
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MapSelectedV2;
|
||||||
542
components/Map/MapsV2Custom.tsx
Normal file
542
components/Map/MapsV2Custom.tsx
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
import { ReactNode, useCallback, useMemo, useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
StyleProp,
|
||||||
|
Animated,
|
||||||
|
Easing,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
MapView,
|
||||||
|
PointAnnotation,
|
||||||
|
} from "@maplibre/maplibre-react-native";
|
||||||
|
|
||||||
|
// Style peta default
|
||||||
|
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
|
||||||
|
// Region default (Bali, Indonesia)
|
||||||
|
const DEFAULT_REGION = {
|
||||||
|
latitude: -8.737109,
|
||||||
|
longitude: 115.1756897,
|
||||||
|
latitudeDelta: 0.1,
|
||||||
|
longitudeDelta: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zoom level default
|
||||||
|
const DEFAULT_ZOOM_LEVEL = 12;
|
||||||
|
|
||||||
|
// Ukuran marker default
|
||||||
|
const DEFAULT_MARKER_SIZE = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface data marker untuk MapsV2Custom
|
||||||
|
*/
|
||||||
|
export interface MapMarker {
|
||||||
|
id: string;
|
||||||
|
coordinate: [number, number]; // [longitude, latitude]
|
||||||
|
imageId?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
onSelected?: () => void;
|
||||||
|
[key: string]: any; // Izinkan properti custom tambahan
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface region untuk positioning kamera
|
||||||
|
*/
|
||||||
|
export interface Region {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
latitudeDelta: number;
|
||||||
|
longitudeDelta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props untuk komponen MapsV2Custom
|
||||||
|
*/
|
||||||
|
export interface MapsV2CustomProps {
|
||||||
|
/** URL style peta custom (default: liberty style) */
|
||||||
|
mapStyle?: string;
|
||||||
|
|
||||||
|
/** Override style container */
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
|
||||||
|
/** Override style MapView */
|
||||||
|
mapViewStyle?: StyleProp<ViewStyle>;
|
||||||
|
|
||||||
|
/** Region awal kamera */
|
||||||
|
initialRegion?: Region;
|
||||||
|
|
||||||
|
/** Zoom level awal (default: 12) */
|
||||||
|
zoomLevel?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data marker - mendukung single marker atau array of markers
|
||||||
|
* @example
|
||||||
|
* // Single marker
|
||||||
|
* markers={{ id: "1", coordinate: [115.175, -8.737], imageId: "abc" }}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Multiple markers
|
||||||
|
* markers={[
|
||||||
|
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||||
|
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||||
|
* ]}
|
||||||
|
*/
|
||||||
|
markers?: MapMarker | MapMarker[];
|
||||||
|
|
||||||
|
/** Custom renderer marker */
|
||||||
|
renderMarker?: (marker: MapMarker) => ReactNode;
|
||||||
|
|
||||||
|
/** Callback ketika marker ditekan */
|
||||||
|
onMarkerPress?: (marker: MapMarker) => void;
|
||||||
|
|
||||||
|
/** Gunakan style marker image default (default: true jika markers disediakan) */
|
||||||
|
showDefaultMarkers?: boolean;
|
||||||
|
|
||||||
|
/** Ukuran untuk marker default (default: 30) */
|
||||||
|
markerSize?: number;
|
||||||
|
|
||||||
|
/** Warna border untuk marker default */
|
||||||
|
markerBorderColor?: string;
|
||||||
|
|
||||||
|
/** Children tambahan untuk MapView (custom overlays, dll.) */
|
||||||
|
children?: ReactNode;
|
||||||
|
|
||||||
|
/** Handler untuk tekan pada peta */
|
||||||
|
onMapPress?: (coordinates: [number, number]) => void;
|
||||||
|
|
||||||
|
/** Test identifier */
|
||||||
|
testID?: string;
|
||||||
|
|
||||||
|
/** Props tambahan untuk Camera */
|
||||||
|
cameraProps?: Partial<Omit<React.ComponentProps<typeof Camera>, "centerCoordinate" | "zoomLevel">>;
|
||||||
|
|
||||||
|
/** Props tambahan untuk MapView */
|
||||||
|
mapViewProps?: Partial<React.ComponentProps<typeof MapView>>;
|
||||||
|
|
||||||
|
/** Props tambahan untuk PointAnnotation */
|
||||||
|
annotationProps?: Partial<{
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
snippet?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
coordinate: number[];
|
||||||
|
anchor?: { x: number; y: number };
|
||||||
|
onSelected?: (payload: any) => void;
|
||||||
|
onDeselected?: (payload: any) => void;
|
||||||
|
onDragStart?: (payload: any) => void;
|
||||||
|
onDragEnd?: (payload: any) => void;
|
||||||
|
onDrag?: (payload: any) => void;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisasi markers ke array - mendukung single marker atau array
|
||||||
|
*/
|
||||||
|
function normalizeMarkers(markers: MapMarker | MapMarker[] | undefined): MapMarker[] {
|
||||||
|
if (!markers) return [];
|
||||||
|
return Array.isArray(markers) ? markers : [markers];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi marker memiliki props yang required (hanya development mode)
|
||||||
|
*/
|
||||||
|
function validateMarker(marker: MapMarker, index: number): boolean {
|
||||||
|
if (!marker.id) {
|
||||||
|
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'id' yang required`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!marker.coordinate || !Array.isArray(marker.coordinate) || marker.coordinate.length !== 2) {
|
||||||
|
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'coordinate' yang required. Format yang diharapkan: [longitude, latitude]`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen skeleton untuk loading state dengan shimmer animation
|
||||||
|
*/
|
||||||
|
function SkeletonMarker({
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
loadingColor = "#C5C5C5",
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
borderColor?: string;
|
||||||
|
loadingColor?: string;
|
||||||
|
}) {
|
||||||
|
const shimmerAnim = useMemo(() => new Animated.Value(0), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const animation = Animated.loop(
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(shimmerAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 800,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(shimmerAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 800,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
animation.start();
|
||||||
|
|
||||||
|
return () => animation.stop();
|
||||||
|
}, [shimmerAnim]);
|
||||||
|
|
||||||
|
const shimmerOpacity = shimmerAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0.3, 0.7],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: loadingColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.skeletonShimmer,
|
||||||
|
{
|
||||||
|
opacity: shimmerOpacity,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen fallback untuk error state
|
||||||
|
*/
|
||||||
|
function FallbackMarker({
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
iconColor = MainColor.darkblue,
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
borderColor?: string;
|
||||||
|
iconColor?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[styles.fallbackIcon, { borderColor: iconColor }]}>
|
||||||
|
<View style={[styles.fallbackIconInner, { backgroundColor: iconColor }]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props untuk DefaultMarker component
|
||||||
|
*/
|
||||||
|
export interface DefaultMarkerProps {
|
||||||
|
/** ID file image dari API */
|
||||||
|
imageId?: string;
|
||||||
|
/** URL image langsung */
|
||||||
|
imageUrl?: string;
|
||||||
|
/** Ukuran marker (default: 30) */
|
||||||
|
size?: number;
|
||||||
|
/** Warna border marker (default: darkblue) */
|
||||||
|
borderColor?: string;
|
||||||
|
/** Warna skeleton loading (default: gray) */
|
||||||
|
loadingColor?: string;
|
||||||
|
/** Warna icon fallback (default: darkblue) */
|
||||||
|
fallbackIconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen marker default dengan image, border, shadows, loading skeleton, dan error fallback
|
||||||
|
*/
|
||||||
|
export function DefaultMarker({
|
||||||
|
imageId,
|
||||||
|
imageUrl,
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
loadingColor = MainColor.white_gray,
|
||||||
|
fallbackIconColor = MainColor.darkblue,
|
||||||
|
}: DefaultMarkerProps) {
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
const uri = imageId ? API_IMAGE.GET({ fileId: imageId }) : imageUrl;
|
||||||
|
|
||||||
|
// Debug log untuk development
|
||||||
|
if (__DEV__ && uri) {
|
||||||
|
console.log("[DefaultMarker] Image URI:", uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = useCallback((error: any) => {
|
||||||
|
console.log("[DefaultMarker] Image error:", error?.nativeEvent?.error || error);
|
||||||
|
setHasError(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoad = useCallback(() => {
|
||||||
|
console.log("[DefaultMarker] Image loaded successfully");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Jika tidak ada URI atau error, tampilkan fallback
|
||||||
|
if (!uri || hasError) {
|
||||||
|
return (
|
||||||
|
<FallbackMarker
|
||||||
|
size={size}
|
||||||
|
borderColor={borderColor}
|
||||||
|
iconColor={fallbackIconColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render image dengan placeholder (defaultSource) untuk loading state
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: loadingColor, // Background color sebagai placeholder
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri }}
|
||||||
|
style={[styles.markerImage, { width: size, height: size }]}
|
||||||
|
resizeMode="cover"
|
||||||
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
|
// Placeholder untuk Android saat loading
|
||||||
|
defaultSource={require("@/assets/images/icon.png")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen Map yang reusable dan customizable menggunakan Mapbox/MapLibre
|
||||||
|
*
|
||||||
|
* Mendukung single marker, multiple markers, atau empty state.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Single marker
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={{
|
||||||
|
* id: "1",
|
||||||
|
* coordinate: [115.1756897, -8.737109],
|
||||||
|
* imageId: "file-123"
|
||||||
|
* }}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Multiple markers
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={[
|
||||||
|
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||||
|
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||||
|
* ]}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Peta dengan custom style dan custom markers
|
||||||
|
* <MapsV2Custom
|
||||||
|
* mapStyle="https://your-custom-style.com"
|
||||||
|
* markers={markers}
|
||||||
|
* markerSize={40}
|
||||||
|
* markerBorderColor={MainColor.primary}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Dengan custom marker renderer
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={data}
|
||||||
|
* renderMarker={(marker) => <CustomMarker {...marker} />}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Peta kosong (tanpa markers)
|
||||||
|
* <MapsV2Custom
|
||||||
|
* initialRegion={{ latitude: -6.2, longitude: 106.8, latitudeDelta: 0.1, longitudeDelta: 0.1 }}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
export function MapsV2Custom({
|
||||||
|
mapStyle = DEFAULT_MAP_STYLE,
|
||||||
|
style = styles.container,
|
||||||
|
mapViewStyle = styles.map,
|
||||||
|
initialRegion = DEFAULT_REGION,
|
||||||
|
zoomLevel = DEFAULT_ZOOM_LEVEL,
|
||||||
|
markers,
|
||||||
|
renderMarker,
|
||||||
|
onMarkerPress,
|
||||||
|
showDefaultMarkers = true,
|
||||||
|
markerSize = DEFAULT_MARKER_SIZE,
|
||||||
|
markerBorderColor = MainColor.darkblue,
|
||||||
|
children,
|
||||||
|
onMapPress,
|
||||||
|
testID,
|
||||||
|
cameraProps,
|
||||||
|
mapViewProps,
|
||||||
|
annotationProps,
|
||||||
|
}: MapsV2CustomProps) {
|
||||||
|
// Normalisasi markers ke array (mendukung single atau multiple)
|
||||||
|
const normalizedMarkers = useMemo(
|
||||||
|
() => {
|
||||||
|
const arr = normalizeMarkers(markers);
|
||||||
|
// Filter marker yang invalid
|
||||||
|
return arr.filter((marker) => {
|
||||||
|
if (!marker.id) {
|
||||||
|
console.warn("[MapsV2Custom] Marker tanpa id akan diabaikan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!marker.coordinate || marker.coordinate.length !== 2) {
|
||||||
|
console.warn("[MapsV2Custom] Marker tanpa coordinate valid akan diabaikan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[markers]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validasi markers dalam development mode
|
||||||
|
useMemo(() => {
|
||||||
|
if (__DEV__) {
|
||||||
|
normalizedMarkers.forEach((marker, index) => {
|
||||||
|
validateMarker(marker, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [normalizedMarkers]);
|
||||||
|
|
||||||
|
const handleMarkerSelected = useCallback(
|
||||||
|
(marker: MapMarker) => {
|
||||||
|
if (marker.onSelected) {
|
||||||
|
marker.onSelected();
|
||||||
|
}
|
||||||
|
if (onMarkerPress) {
|
||||||
|
onMarkerPress(marker);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onMarkerPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderMarkerComponent = useCallback(
|
||||||
|
(marker: MapMarker): ReactNode => {
|
||||||
|
if (renderMarker) {
|
||||||
|
return renderMarker(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDefaultMarkers) {
|
||||||
|
return (
|
||||||
|
<DefaultMarker
|
||||||
|
imageId={marker.imageId}
|
||||||
|
imageUrl={marker.imageUrl}
|
||||||
|
size={markerSize}
|
||||||
|
borderColor={markerBorderColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[renderMarker, showDefaultMarkers, markerSize, markerBorderColor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style} testID={testID}>
|
||||||
|
<MapView style={mapViewStyle} mapStyle={mapStyle} {...mapViewProps}>
|
||||||
|
<Camera
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
centerCoordinate={[initialRegion.longitude, initialRegion.latitude]}
|
||||||
|
{...cameraProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{normalizedMarkers.map((marker) => (
|
||||||
|
<PointAnnotation
|
||||||
|
key={marker.id}
|
||||||
|
id={marker.id}
|
||||||
|
coordinate={marker.coordinate}
|
||||||
|
onSelected={() => handleMarkerSelected(marker)}
|
||||||
|
{...annotationProps}
|
||||||
|
>
|
||||||
|
{renderMarkerComponent(marker) as any}
|
||||||
|
</PointAnnotation>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
map: { flex: 1 },
|
||||||
|
markerContainer: {
|
||||||
|
overflow: "hidden",
|
||||||
|
borderWidth: 1,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 3,
|
||||||
|
},
|
||||||
|
markerImage: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
skeletonShimmer: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
fallbackIcon: {
|
||||||
|
width: "60%",
|
||||||
|
height: "60%",
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 2,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
fallbackIconInner: {
|
||||||
|
width: "40%",
|
||||||
|
height: "40%",
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
});
|
||||||
272
components/Map/SelectLocationMap.tsx
Normal file
272
components/Map/SelectLocationMap.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import React, { useState, useCallback, useRef } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from "react-native";
|
||||||
|
import {
|
||||||
|
MapView,
|
||||||
|
Camera,
|
||||||
|
PointAnnotation,
|
||||||
|
MarkerView,
|
||||||
|
} from "@maplibre/maplibre-react-native";
|
||||||
|
import * as Location from "expo-location";
|
||||||
|
import { useFocusEffect, useRouter } from "expo-router";
|
||||||
|
|
||||||
|
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
|
||||||
|
type Coordinate = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SelectLocationMap() {
|
||||||
|
const router = useRouter();
|
||||||
|
const annotationRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const [selectedCoord, setSelectedCoord] = useState<Coordinate | null>(null);
|
||||||
|
const [address, setAddress] = useState<string>("");
|
||||||
|
const [isLoadingAddress, setIsLoadingAddress] = useState(false);
|
||||||
|
const [cameraCenter, setCameraCenter] = useState<[number, number]>([
|
||||||
|
106.8272, -6.1751,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const reverseGeocode = async (coord: Coordinate): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const { status } = await Location.getForegroundPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
await Location.requestForegroundPermissionsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Location.reverseGeocodeAsync({
|
||||||
|
latitude: coord.latitude,
|
||||||
|
longitude: coord.longitude,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!results || results.length === 0) return "Alamat tidak ditemukan";
|
||||||
|
|
||||||
|
const loc = results[0];
|
||||||
|
const parts = [
|
||||||
|
loc.street,
|
||||||
|
loc.district,
|
||||||
|
loc.subregion,
|
||||||
|
loc.city,
|
||||||
|
loc.region,
|
||||||
|
loc.country,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(", ") : "Alamat tidak ditemukan";
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("reverseGeocode error:", error?.message || error);
|
||||||
|
return "Gagal mengambil alamat";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMapPress = useCallback(async (event: any) => {
|
||||||
|
try {
|
||||||
|
const coordinates = event?.geometry?.coordinates;
|
||||||
|
if (!coordinates) return;
|
||||||
|
|
||||||
|
const [longitude, latitude] = coordinates;
|
||||||
|
if (!longitude || !latitude) return;
|
||||||
|
|
||||||
|
const coord: Coordinate = { latitude, longitude };
|
||||||
|
|
||||||
|
// ✅ Update state koordinat, BUKAN ganti key
|
||||||
|
setSelectedCoord(coord);
|
||||||
|
setCameraCenter([longitude, latitude]);
|
||||||
|
setAddress("");
|
||||||
|
setIsLoadingAddress(true);
|
||||||
|
|
||||||
|
const resolvedAddress = await reverseGeocode(coord);
|
||||||
|
setAddress(resolvedAddress);
|
||||||
|
setIsLoadingAddress(false);
|
||||||
|
|
||||||
|
// ✅ Refresh annotation tanpa unmount
|
||||||
|
annotationRef.current?.refresh?.();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("handleMapPress error:", error?.message || error);
|
||||||
|
setIsLoadingAddress(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedCoord) return;
|
||||||
|
router.navigate({
|
||||||
|
pathname: "/maps/create",
|
||||||
|
params: {
|
||||||
|
latitude: String(selectedCoord.latitude),
|
||||||
|
longitude: String(selectedCoord.longitude),
|
||||||
|
address,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sembunyikan marker sebelum halaman unmount
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
return () => {
|
||||||
|
// Cleanup saat leave — sembunyikan marker dulu sebelum unmount
|
||||||
|
setSelectedCoord(null);
|
||||||
|
};
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<MapView style={styles.map} mapStyle={MAP_STYLE} onPress={handleMapPress}>
|
||||||
|
<Camera
|
||||||
|
zoomLevel={14}
|
||||||
|
centerCoordinate={cameraCenter}
|
||||||
|
animationMode="flyTo"
|
||||||
|
animationDuration={300}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ✅ Key statis — tidak pernah berubah, tidak unmount/remount */}
|
||||||
|
{selectedCoord && (
|
||||||
|
<MarkerView
|
||||||
|
id="selected-marker"
|
||||||
|
coordinate={[selectedCoord.longitude, selectedCoord.latitude]}
|
||||||
|
anchor={{ x: 0.5, y: 1 }} // Anchor bawah tengah
|
||||||
|
>
|
||||||
|
<View style={styles.pin}>
|
||||||
|
<View style={styles.pinDot} />
|
||||||
|
</View>
|
||||||
|
</MarkerView>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
|
||||||
|
{/* Bottom Sheet */}
|
||||||
|
<View style={styles.bottomSheet}>
|
||||||
|
{!selectedCoord ? (
|
||||||
|
<Text style={styles.hintText}>
|
||||||
|
Tap pada peta untuk memilih lokasi
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={styles.coordRow}>
|
||||||
|
<View style={styles.coordItem}>
|
||||||
|
<Text style={styles.coordLabel}>Latitude</Text>
|
||||||
|
<Text style={styles.coordValue}>
|
||||||
|
{selectedCoord.latitude.toFixed(6)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.dividerVertical} />
|
||||||
|
<View style={styles.coordItem}>
|
||||||
|
<Text style={styles.coordLabel}>Longitude</Text>
|
||||||
|
<Text style={styles.coordValue}>
|
||||||
|
{selectedCoord.longitude.toFixed(6)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.addressContainer}>
|
||||||
|
<Text style={styles.coordLabel}>Alamat</Text>
|
||||||
|
{isLoadingAddress ? (
|
||||||
|
<ActivityIndicator size="small" color="#0a1f44" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.addressText} numberOfLines={2}>
|
||||||
|
{address || "-"}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.confirmButton,
|
||||||
|
isLoadingAddress && styles.confirmButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleConfirm}
|
||||||
|
disabled={isLoadingAddress}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmButtonText}>Konfirmasi Lokasi</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
map: { flex: 1 },
|
||||||
|
pin: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: "#0a1f44",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#fff",
|
||||||
|
},
|
||||||
|
pinDot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
bottomSheet: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 20,
|
||||||
|
paddingBottom: 32,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
elevation: 10,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: -3 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 6,
|
||||||
|
minHeight: 140,
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
hintText: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#888",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
coordRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
coordItem: { flex: 1 },
|
||||||
|
dividerVertical: {
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
marginHorizontal: 12,
|
||||||
|
},
|
||||||
|
coordLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#888",
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
coordValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#0a1f44",
|
||||||
|
},
|
||||||
|
addressContainer: { marginBottom: 16 },
|
||||||
|
addressText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#333",
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
confirmButton: {
|
||||||
|
backgroundColor: "#0a1f44",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
confirmButtonDisabled: {
|
||||||
|
backgroundColor: "#aaa",
|
||||||
|
},
|
||||||
|
confirmButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Device token berhasil didaftarkan ke backend");
|
console.log("✅ Device token berhasil didaftarkan ke backend");
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("❌ Gagal mendaftarkan device token:", error);
|
// Log error detail tapi jangan crash aplikasi
|
||||||
|
console.error("❌ Gagal mendaftarkan device token:", error?.message);
|
||||||
|
console.error("Response status:", error?.response?.status);
|
||||||
|
console.error("Response data:", error?.response?.data);
|
||||||
|
// Skip logout - biarkan user tetap bisa pakai app meski notif gagal
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import BaseBox from "@/components/Box/BaseBox";
|
||||||
|
import TextCustom from "@/components/Text/TextCustom";
|
||||||
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
|
import { StyleProp, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onPress?: () => void;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminBasicBox({ children, onPress, style }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox onPress={onPress} style={style}>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import BaseBox from "@/components/Box/BaseBox";
|
import BaseBox from "@/components/Box/BaseBox";
|
||||||
import Grid from "@/components/Grid/GridCustom";
|
import Grid from "@/components/Grid/GridCustom";
|
||||||
import TextCustom from "@/components/Text/TextCustom";
|
import TextCustom from "@/components/Text/TextCustom";
|
||||||
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
|
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
export default function AdminComp_BoxTitle({
|
export default function AdminComp_BoxTitle({
|
||||||
title,
|
title,
|
||||||
@@ -12,13 +14,33 @@ export default function AdminComp_BoxTitle({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseBox
|
{/* <BaseBox
|
||||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
||||||
paddingTop={5}
|
paddingTop={5}
|
||||||
paddingBottom={5}
|
paddingBottom={5}
|
||||||
|
backgroundColor={AccentColor.blue}
|
||||||
|
> */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: AccentColor.darkblue,
|
||||||
|
borderColor: AccentColor.blue,
|
||||||
|
paddingBlock: 5,
|
||||||
|
paddingInline: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Grid>
|
<Grid
|
||||||
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
|
// containerStyle={{
|
||||||
|
// bottom: 0,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<Grid.Col
|
||||||
|
span={rightComponent ? 6 : 12}
|
||||||
|
style={{ justifyContent: "center" }}
|
||||||
|
>
|
||||||
<TextCustom
|
<TextCustom
|
||||||
// style={{ alignSelf: "center" }}
|
// style={{ alignSelf: "center" }}
|
||||||
bold
|
bold
|
||||||
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</BaseBox>
|
</View>
|
||||||
|
{/* </BaseBox> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
114
components/_ShareComponent/AppHeader.tsx
Normal file
114
components/_ShareComponent/AppHeader.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { BackButton } from "..";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
right?: React.ReactNode;
|
||||||
|
showBack?: boolean;
|
||||||
|
onPressLeft?: () => void;
|
||||||
|
left?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppHeader({
|
||||||
|
title,
|
||||||
|
right,
|
||||||
|
showBack = true,
|
||||||
|
onPressLeft,
|
||||||
|
left,
|
||||||
|
}: Props) {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
// iOS 16+ detection (Dynamic Island) - insets.top > 47 indicates Dynamic Island
|
||||||
|
const isIOS26Plus =
|
||||||
|
Platform.OS === "ios" && insets.top > 47;
|
||||||
|
|
||||||
|
// Dynamic padding berdasarkan platform dan iOS version
|
||||||
|
const paddingTop =
|
||||||
|
Platform.OS === "ios"
|
||||||
|
? isIOS26Plus
|
||||||
|
? insets.top - 10
|
||||||
|
: insets.top
|
||||||
|
: 10;
|
||||||
|
|
||||||
|
const paddingBottom = Platform.OS === "ios" ? 8 : 13;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
backgroundColor: MainColor.darkblue,
|
||||||
|
paddingTop,
|
||||||
|
paddingBottom,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
>
|
||||||
|
{/* Header Container dengan absolute positioning untuk title center */}
|
||||||
|
<View style={styles.headerApp} pointerEvents="box-none">
|
||||||
|
{/* Left Section - Absolute Left */}
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
{showBack ? (
|
||||||
|
<BackButton onPress={onPressLeft} />
|
||||||
|
) : left ? (
|
||||||
|
left
|
||||||
|
) : (
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title - Absolute Center */}
|
||||||
|
<View style={styles.headerCenter}>
|
||||||
|
<Text
|
||||||
|
style={styles.headerTitle}
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode="tail"
|
||||||
|
>
|
||||||
|
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Right Section - Absolute Right */}
|
||||||
|
<View style={styles.headerRight}>
|
||||||
|
{right}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerApp: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
height: 44, // Fixed height untuk consistency
|
||||||
|
},
|
||||||
|
headerLeft: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 16,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
headerCenter: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
headerRight: {
|
||||||
|
position: "absolute",
|
||||||
|
right: 16,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
color: MainColor.yellow,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
export default function BasicWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,15 +4,15 @@ import Grid from "../Grid/GridCustom";
|
|||||||
export default function GridTwoView({
|
export default function GridTwoView({
|
||||||
spanLeft = 6,
|
spanLeft = 6,
|
||||||
spanRight = 6,
|
spanRight = 6,
|
||||||
leftIcon,
|
leftItem,
|
||||||
rightIcon,
|
rightItem,
|
||||||
styleLeft,
|
styleLeft,
|
||||||
styleRight,
|
styleRight,
|
||||||
}: {
|
}: {
|
||||||
spanLeft?: number;
|
spanLeft?: number;
|
||||||
spanRight?: number;
|
spanRight?: number;
|
||||||
leftIcon?: React.ReactNode;
|
leftItem?: React.ReactNode;
|
||||||
rightIcon?: React.ReactNode;
|
rightItem?: React.ReactNode;
|
||||||
styleLeft?: ViewStyle;
|
styleLeft?: ViewStyle;
|
||||||
styleRight?: ViewStyle;
|
styleRight?: ViewStyle;
|
||||||
}) {
|
}) {
|
||||||
@@ -24,13 +24,13 @@ export default function GridTwoView({
|
|||||||
span={spanLeft}
|
span={spanLeft}
|
||||||
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
|
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
|
||||||
>
|
>
|
||||||
{leftIcon}
|
{leftItem}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col
|
<Grid.Col
|
||||||
span={spanRight}
|
span={spanRight}
|
||||||
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
|
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
|
||||||
>
|
>
|
||||||
{rightIcon}
|
{rightItem}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
|
|||||||
right: 0,
|
right: 0,
|
||||||
height: 100,
|
height: 100,
|
||||||
backgroundColor: MainColor.soft_darkblue,
|
backgroundColor: MainColor.soft_darkblue,
|
||||||
borderRadius: 4,
|
borderRadius: 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
|
|||||||
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
||||||
import GridComponentView from "./_ShareComponent/GridSectionView";
|
import GridComponentView from "./_ShareComponent/GridSectionView";
|
||||||
import NewWrapper from "./_ShareComponent/NewWrapper";
|
import NewWrapper from "./_ShareComponent/NewWrapper";
|
||||||
|
import BasicWrapper from "./_ShareComponent/BasicWrapper";
|
||||||
// Progress
|
// Progress
|
||||||
import ProgressCustom from "./Progress/ProgressCustom";
|
import ProgressCustom from "./Progress/ProgressCustom";
|
||||||
// Loader
|
// Loader
|
||||||
@@ -121,6 +122,7 @@ export {
|
|||||||
GridComponentView,
|
GridComponentView,
|
||||||
Spacing,
|
Spacing,
|
||||||
NewWrapper,
|
NewWrapper,
|
||||||
|
BasicWrapper,
|
||||||
// Stack
|
// Stack
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TabBarBackground,
|
TabBarBackground,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type AuthContextType = {
|
|||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isUserActive: boolean;
|
isUserActive: boolean;
|
||||||
loginWithNomor: (nomor: string) => Promise<boolean>;
|
loginWithNomor: (nomor: string) => Promise<boolean>;
|
||||||
validateOtp: (nomor: string) => Promise<any>;
|
validateOtp: (nomor: string, code: string) => Promise<any>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
registerUser: (userData: {
|
registerUser: (userData: {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -97,10 +97,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- 2. Validasi OTP & cek user ---
|
// --- 2. Validasi OTP & cek user ---
|
||||||
const validateOtp = async (nomor: string) => {
|
const validateOtp = async (nomor: string, code: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await apiValidationCode({ nomor: nomor });
|
const response = await apiValidationCode({ nomor: nomor, code: code });
|
||||||
const { token } = response;
|
const { token } = response;
|
||||||
console.log("[RESPONSE VALIDASI OTP]", JSON.stringify(response, null, 2));
|
console.log("[RESPONSE VALIDASI OTP]", JSON.stringify(response, null, 2));
|
||||||
|
|
||||||
|
|||||||
28
docs/CHANGE_LOG.md
Normal file
28
docs/CHANGE_LOG.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# CHANGE LOG - fixed-admin/18-feb-26
|
||||||
|
|
||||||
|
## Perubahan Tampilan Admin
|
||||||
|
|
||||||
|
### File Baru (4)
|
||||||
|
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
|
||||||
|
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
|
||||||
|
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
|
||||||
|
- `screens/Admin/Voting/BoxVotingStatus.tsx`
|
||||||
|
|
||||||
|
### File Diubah (3)
|
||||||
|
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
|
||||||
|
- `app/(application)/admin/voting/history.tsx` → 5 baris
|
||||||
|
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris
|
||||||
|
|
||||||
|
### API Updates (2)
|
||||||
|
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
|
||||||
|
- `service/api-admin/api-master-admin.ts` → tambah param `page`
|
||||||
|
|
||||||
|
## Fitur Baru
|
||||||
|
- Pagination (infinite scroll)
|
||||||
|
- Pull-to-Refresh
|
||||||
|
- Skeleton Loading
|
||||||
|
- Empty State
|
||||||
|
- Search Functionality
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
+305 baris, -531 baris (net: -226)
|
||||||
101
docs/PODS.back
Normal file
101
docs/PODS.back
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
NOTE:
|
||||||
|
|
||||||
|
Untuk Development Selanjutnya:
|
||||||
|
Sekarang Anda bisa menjalankan:
|
||||||
|
|
||||||
|
1 # Untuk run iOS dev client
|
||||||
|
2 bun run ios
|
||||||
|
3
|
||||||
|
4 # Atau dengan Expo
|
||||||
|
5 bunx expo run:ios
|
||||||
|
|
||||||
|
Jika di masa depan terjadi error serupa, Anda bisa gunakan command ini:
|
||||||
|
|
||||||
|
1 cd ios
|
||||||
|
2 rm -rf Pods Podfile.lock
|
||||||
|
3 pod install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||||
|
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||||
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
|
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||||
|
|
||||||
|
prepare_react_native_project!
|
||||||
|
|
||||||
|
target 'HIPMIBadungConnect' do
|
||||||
|
use_expo_modules!
|
||||||
|
|
||||||
|
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||||
|
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||||
|
else
|
||||||
|
config_command = [
|
||||||
|
'npx',
|
||||||
|
'expo-modules-autolinking',
|
||||||
|
'react-native-config',
|
||||||
|
'--json',
|
||||||
|
'--platform',
|
||||||
|
'ios'
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
config = use_native_modules!(config_command)
|
||||||
|
|
||||||
|
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||||
|
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||||
|
|
||||||
|
|
||||||
|
use_react_native!(
|
||||||
|
:path => config[:reactNativePath],
|
||||||
|
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||||
|
# An absolute path to your application root.
|
||||||
|
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||||
|
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||||
|
)
|
||||||
|
|
||||||
|
pod 'Firebase'
|
||||||
|
pod 'Firebase/Messaging'
|
||||||
|
|
||||||
|
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
|
||||||
|
post_install do |installer|
|
||||||
|
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
|
||||||
|
$MLRN.post_install(installer)
|
||||||
|
# @generated end @maplibre/maplibre-react-native:post-install
|
||||||
|
|
||||||
|
# Fix all script phases with incorrect paths
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_phases.each do |phase|
|
||||||
|
next unless phase.respond_to?(:shell_script)
|
||||||
|
|
||||||
|
# Fix duplicated path issue
|
||||||
|
if phase.shell_script.include?('with-environment.sh')
|
||||||
|
# Remove any existing path and use proper relative path
|
||||||
|
phase.shell_script = phase.shell_script.gsub(
|
||||||
|
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
|
||||||
|
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Standard React Native post install
|
||||||
|
react_native_post_install(
|
||||||
|
installer,
|
||||||
|
config[:reactNativePath],
|
||||||
|
:mac_catalyst_enabled => false,
|
||||||
|
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# @generated end post_installer
|
||||||
|
|
||||||
|
end
|
||||||
253
docs/QR_CODE_TESTING.md
Normal file
253
docs/QR_CODE_TESTING.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# QR Code Testing Guide - HIPMI Mobile
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
Dokumentasi ini menjelaskan cara testing QR Code untuk Universal Links (iOS) dan App Links (Android) pada fitur Event Confirmation.
|
||||||
|
|
||||||
|
## 🔧 Update Terbaru
|
||||||
|
|
||||||
|
File `screens/Admin/Event/EventDetailQRCode.tsx` telah diupdate dengan fitur:
|
||||||
|
- **Toggle Button**: Switch antara HTTPS link dan Custom Scheme link
|
||||||
|
- **HTTPS Link**: Untuk testing Universal Links/App Links dengan domain staging
|
||||||
|
- **Custom Scheme**: Untuk testing langsung tanpa domain verification
|
||||||
|
|
||||||
|
## 🎯 Cara Testing QR Code
|
||||||
|
|
||||||
|
### Opsi 1: HTTPS Link (Recommended untuk Production)
|
||||||
|
|
||||||
|
**Gunakan tombol "HTTPS"** di component QR Code.
|
||||||
|
|
||||||
|
**Link yang di-generate:**
|
||||||
|
```
|
||||||
|
https://cld-dkr-staging-hipmi.wibudev.com/event/{id}/confirmation?userId={userId}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cara kerja:**
|
||||||
|
1. User scan QR code dengan kamera
|
||||||
|
2. Safari/Chrome terbuka dengan URL HTTPS
|
||||||
|
3. iOS/Android mendeteksi domain terverifikasi
|
||||||
|
4. App terbuka otomatis dan menuju halaman confirmation
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- ✅ File `apple-app-site-association` harus accessible di Next.js server
|
||||||
|
- ✅ File `assetlinks.json` harus accessible di Next.js server
|
||||||
|
- ✅ Domain harus terverifikasi di app.config.js
|
||||||
|
- ✅ App harus di-build ulang setelah perubahan domain
|
||||||
|
|
||||||
|
**Testing Steps:**
|
||||||
|
```bash
|
||||||
|
# 1. Pastikan .well-known files accessible
|
||||||
|
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||||
|
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
|
||||||
|
|
||||||
|
# 2. Rebuild app
|
||||||
|
bunx expo prebuild --clean
|
||||||
|
|
||||||
|
# 3. Run di physical device (bukan simulator)
|
||||||
|
bun run android # untuk Android
|
||||||
|
bun run ios # untuk iOS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opsi 2: Custom Scheme Link (Untuk Development/Testing Cepat)
|
||||||
|
|
||||||
|
**Gunakan tombol "Custom Scheme"** di component QR Code.
|
||||||
|
|
||||||
|
**Link yang di-generate:**
|
||||||
|
```
|
||||||
|
hipmimobile://event/{id}/confirmation?userId={userId}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cara kerja:**
|
||||||
|
1. User scan QR code dengan kamera
|
||||||
|
2. iOS: Pilih "Open in HIPMI Badung Connect"
|
||||||
|
3. Android: Langsung buka app
|
||||||
|
4. App terbuka dan menuju halaman confirmation
|
||||||
|
|
||||||
|
**Keuntungan:**
|
||||||
|
- ✅ Tidak butuh domain verification
|
||||||
|
- ✅ Bisa testing langsung tanpa rebuild
|
||||||
|
- ✅ Cocok untuk development
|
||||||
|
|
||||||
|
**Kekurangan:**
|
||||||
|
- ❌ Tidak bisa dibuka dari web browser
|
||||||
|
- ❌ Tidak support universal linking dari website lain
|
||||||
|
|
||||||
|
## 📱 Testing Checklist
|
||||||
|
|
||||||
|
### iOS (Universal Links)
|
||||||
|
|
||||||
|
- [ ] File `apple-app-site-association` valid dan accessible
|
||||||
|
- [ ] Domain terdaftar di `app.config.js` → `ios.associatedDomains`
|
||||||
|
- [ ] Bundle ID match dengan konfigurasi
|
||||||
|
- [ ] Team ID benar di apple-app-site-association
|
||||||
|
- [ ] Test dengan **physical device** (simulator tidak support)
|
||||||
|
- [ ] Test dengan **Safari** (bukan Chrome)
|
||||||
|
- [ ] Long press link → ada opsi "Open"
|
||||||
|
|
||||||
|
**Debug iOS:**
|
||||||
|
```bash
|
||||||
|
# Cek apple-app-site-association
|
||||||
|
curl -I https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||||
|
|
||||||
|
# Harus return:
|
||||||
|
# Content-Type: application/json
|
||||||
|
# HTTP/2 200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android (App Links)
|
||||||
|
|
||||||
|
- [ ] File `assetlinks.json` valid dan accessible
|
||||||
|
- [ ] SHA256 fingerprint benar
|
||||||
|
- [ ] Package name match
|
||||||
|
- [ ] Intent filters terdaftar di app.config.js
|
||||||
|
- [ ] Test dengan **physical device**
|
||||||
|
- [ ] Test dengan **Chrome**
|
||||||
|
|
||||||
|
**Debug Android:**
|
||||||
|
```bash
|
||||||
|
# Dapatkan SHA256 fingerprint
|
||||||
|
cd android
|
||||||
|
./gradlew signingReport
|
||||||
|
|
||||||
|
# Cek assetlinks.json
|
||||||
|
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Problem: QR Scan Terbuka di Safari, Tidak Balik ke App
|
||||||
|
|
||||||
|
**Penyebab:**
|
||||||
|
- Domain belum terverifikasi untuk Universal Links/App Links
|
||||||
|
- File `.well-known` tidak accessible atau invalid
|
||||||
|
- App belum di-rebuild setelah perubahan domain
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
1. Pastikan file `.well-known` accessible:
|
||||||
|
```bash
|
||||||
|
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rebuild app:
|
||||||
|
```bash
|
||||||
|
bunx expo prebuild --clean
|
||||||
|
bun run android # atau bun run ios
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Gunakan **Custom Scheme** untuk testing cepat
|
||||||
|
|
||||||
|
### Problem: Link Tidak Membuka App Sama Sekali
|
||||||
|
|
||||||
|
**Cek:**
|
||||||
|
1. App sudah terinstall di device
|
||||||
|
2. Link format benar (hipmimobile:// atau https://)
|
||||||
|
3. Route handler sudah ada di app folder
|
||||||
|
|
||||||
|
**Test manual:**
|
||||||
|
```bash
|
||||||
|
# iOS Simulator
|
||||||
|
xcrun simctl openurl booted "hipmimobile://event/123/confirmation?userId=456"
|
||||||
|
|
||||||
|
# Android Emulator
|
||||||
|
adb shell am start -W -a android.intent.action.VIEW \
|
||||||
|
-d "hipmimobile://event/123/confirmation?userId=456" \
|
||||||
|
com.bip.hipmimobileapp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Cannot GET /event/..." di Next.js
|
||||||
|
|
||||||
|
**Penyebab:**
|
||||||
|
Route `/event/[id]/confirmation` tidak ada di Next.js server
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
Pastikan Next.js project punya file:
|
||||||
|
```
|
||||||
|
public/.well-known/apple-app-site-association
|
||||||
|
public/.well-known/assetlinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Dan API route untuk handle:
|
||||||
|
```
|
||||||
|
pages/api/event/[id]/confirmation.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 File Configuration
|
||||||
|
|
||||||
|
### app.config.js - iOS
|
||||||
|
```javascript
|
||||||
|
ios: {
|
||||||
|
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### app.config.js - Android
|
||||||
|
```javascript
|
||||||
|
android: {
|
||||||
|
intentFilters: [
|
||||||
|
{
|
||||||
|
action: "VIEW",
|
||||||
|
autoVerify: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
scheme: "https",
|
||||||
|
host: "cld-dkr-staging-hipmi.wibudev.com",
|
||||||
|
pathPrefix: "/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
category: ["BROWSABLE", "DEFAULT"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### apple-app-site-association (Next.js)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"apps": [],
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appID": "TEAM_ID.com.anonymous.hipmi-mobile",
|
||||||
|
"paths": ["/event/*/confirmation"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### assetlinks.json (Next.js)
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "com.bip.hipmimobileapp",
|
||||||
|
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 Best Practices
|
||||||
|
|
||||||
|
1. **Development**: Gunakan Custom Scheme untuk testing cepat
|
||||||
|
2. **Staging**: Gunakan HTTPS link dengan domain staging
|
||||||
|
3. **Production**: Gunakan HTTPS link dengan domain production
|
||||||
|
4. **Testing**: Selalu test di physical device, bukan simulator
|
||||||
|
5. **Debugging**: Enable logging di confirmation page untuk track deep link
|
||||||
|
|
||||||
|
## 🔗 Related Files
|
||||||
|
|
||||||
|
- `screens/Admin/Event/EventDetailQRCode.tsx` - QR Code generator
|
||||||
|
- `app/(application)/(user)/event/[id]/confirmation.tsx` - Confirmation page
|
||||||
|
- `app.config.js` - App configuration
|
||||||
|
- `service/api-config.ts` - API configuration (DEEP_LINK_URL)
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Jika masih ada masalah:
|
||||||
|
1. Cek logs di console
|
||||||
|
2. Test manual dengan adb/xcrun
|
||||||
|
3. Verify .well-known files dengan curl
|
||||||
|
4. Pastikan app rebuild setelah perubahan config
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user