Compare commits

...

24 Commits

Author SHA1 Message Date
a5026cc285 add: Admin Event detail screen dan komponen pendukung
Deskripsi:

Menambahkan halaman detail event pada admin panel dengan status parameter

Menambahkan beberapa komponen UI untuk menampilkan detail event, drawer informasi, dan QR Code

Update konfigurasi aplikasi dan iOS project

Perbaikan pada halaman verifikasi authentication

Update dokumentasi prompt untuk Qwen

File yang diubah:

Modified

app.config.js

app/(application)/admin/event/[id]/[status]/index.tsx

docs/prompt-for-qwen-code.md

ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

ios/HIPMIBadungConnect/Info.plist

screens/Authentication/VerificationView.tsx

New Admin Event Components

screens/Admin/Event/BoxEventDetail.tsx

screens/Admin/Event/EventDetailDrawer.tsx

screens/Admin/Event/EventDetailQRCode.tsx

screens/Admin/Event/ScreenEventDetail.tsx

### No Issue
2026-03-06 16:39:55 +08:00
836ef709d2 Fix bug home
### No Issue
2026-03-05 16:41:29 +08:00
3bbee15c3a Perbaikan Bug & Error Handling: │
│                                                                                                            │
  │    1. Device Token Registration Error (HTTP 500)                                                           │
  │       - File: service/api-device-token.ts                                                                  │
  │       - Fix: Hapus nested data wrapper pada payload                                                        │
  │       - Improvement: Tambahkan error logging detail                                                        │
  │                                                                                                            │
  │    2. Uncaught Promise Errors                                                                              │
  │       - File: components/Notification/NotificationInitializer.tsx                                          │
  │       - Fix: Better error handling untuk device token registration                                         │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Fix: Add .catch() untuk userData() dan error handling apiUser()                                    │
  │       - File: app/(application)/(user)/profile/[id]/index.tsx                                              │
  │       - Fix: Add error handling untuk apiProfile(), apiUser(), userData()                                  │
  │                                                                                                            │
  │    3. UI Improvements                                                                                      │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Feature: 4 skeleton lingkaran untuk loading state grid features                                    │
  │                                                                                                            │
  │    4. Maps Migration                                                                                       │
  │       - File: app/(application)/admin/maps.tsx                                                             │
  │       - Change: Replace react-native-maps dengan MapsV2Custom (Maplibre)                                   │
  │       - Cleanup: Hapus unused imports dan interfaces                                                       │
  │                                                                                                            │
  │   Files Modified (7)                                                                                       │
  │    - app/(application)/(user)/home.tsx                                                                     │
  │    - app/(application)/(user)/profile/[id]/index.tsx                                                       │
  │    - app/(application)/admin/maps.tsx                                                                      │
  │    - components/Notification/NotificationInitializer.tsx                                                   │
  │    - service/api-device-token.ts                                                                           │
  │    - constants/constans-value.ts                                                                           │
  │    - screens/Home/bottomFeatureSection.tsx                                                                 │
  │    - screens/UserSeach/MainView_V2.tsx

### No Issue
2026-03-04 16:39:57 +08:00
ad7dbaf162 Fix Bug DB
User Pages
- app/(application)/(user)/home.tsx
- app/(application)/(user)/portofolio/[id]/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Components
- components/Notification/NotificationInitializer.tsx
- components/_ShareComponent/SkeletonCustom.tsx

Service
- service/api-device-token.ts

Config & iOS
- app.config.js
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-03-03 16:44:45 +08:00
9c94ec0454 Fix Bug
Maps Platform Update
- components/Map/MapSelectedPlatform.tsx
- components/Map/MapSelectedV2.tsx

Maps Screens
- screens/Maps/ScreenMapsCreate.tsx
- screens/Maps/ScreenMapsEdit.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Config & Native
- app.config.js
- android/app/build.gradle
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-03-02 16:34:24 +08:00
4c63485a5b Clean Code Edit Maps
Maps Edit Feature
- app/(application)/(user)/maps/[id]/edit.tsx
- components/Map/MapSelectedV2.tsx

Docs
- docs/prompt-for-qwen-code.md

New Screen
- screens/Maps/ScreenMapsEdit.tsx

### No Issue
2026-03-02 10:31:29 +08:00
f5d09a2906 Fix create maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-27 16:57:01 +08:00
67070bb2f1 Fix Maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-26 18:04:45 +08:00
fb19ec60b2 Fix Maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-26 17:53:23 +08:00
e8f5c5b174 User Maps
- app/(application)/(user)/maps/index.tsx
- screens/Maps/MapsView2.tsx

New Maps Component
- screens/Maps/DrawerMaps.tsx

Docs / Backup
- docs/PODS.back

### No Issue
2026-02-25 16:46:37 +08:00
74a4d88277 Fix POD File & Maps
User Maps
- app/(application)/(user)/maps/index.tsx
- screens/Maps/MapsView2.tsx

iOS
- HIPMIBadungConnect.xcodeproj/project.pbxproj
- Podfile.lock
- HIPMIBadungConnect.xcworkspace/xcshareddata/

New Maps Component
- screens/Maps/DrawerMaps.tsx

Docs / Backup
- docs/PODS.back

### No Issue
2026-02-25 16:37:42 +08:00
2ad93a26a8 Tampilan maps android
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 17:57:14 +08:00
768b0caa9e Fix maps
### No Issue
2026-02-24 16:48:37 +08:00
208b0ce813 Fix Maps
Config & Dependencies
- app.config.js
- package.json
- bun.lock
- ios/Podfile
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

User Pages
- app/(application)/(user)/maps/index.tsx
- app/(application)/(user)/portofolio/[id]/index.tsx

Maps
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 16:41:43 +08:00
66e6aebf41 Fixed Admin Package
Config & Dependencies
- app.config.js
- package.json
- bun.lock
- ios/Podfile
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

Home
- screens/Home/tabsList.ts

Maps
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 07:18:01 +08:00
32a42d1b60 Fix UI Admin
User & Image
- app/(application)/(image)/take-picture/[id]/index.tsx
- app/(application)/(user)/home.tsx

Admin – Forum
- app/(application)/admin/forum/[id]/index.tsx
- app/(application)/admin/forum/[id]/list-comment.tsx
- app/(application)/admin/forum/[id]/list-report-comment.tsx

Admin Screens – Forum
- screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
- screens/Admin/Forum/ScreenForumReportComment.tsx
- screens/Admin/Forum/ScreenForumReportPosting.tsx

New Admin Screens – Forum
- screens/Admin/Forum/ScreenForumDetailReportComment.tsx
- screens/Admin/Forum/ScreenForumListComment.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Service
- service/api-admin/api-admin-forum.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-20 16:48:26 +08:00
107d4312e1 Fixed UI Admin
Admin Forum Pages
- app/(application)/admin/forum/[id]/list-report-posting.tsx
- app/(application)/admin/forum/posting.tsx
- app/(application)/admin/forum/report-comment.tsx
- app/(application)/admin/forum/report-posting.tsx

Admin Forum Service
- service/api-admin/api-admin-forum.ts

Docs
- docs/prompt-for-qwen-code.md

New Admin Screens
- screens/Admin/Forum/

### No Issue
2026-02-19 16:40:35 +08:00
4862975402 Ringkasan Perubahan
File yang Dimodifikasi:
     1. `service/api-admin/api-admin-investment.ts` - Tambah parameter page
        untuk pagination
     2. `app/(application)/admin/investment/[id]/list-of-investor.tsx` - Clean
         route file

    File Baru:
     3. `screens/Admin/Investment/ScreenInvestmentListOfInvestor.tsx` - Screen
         component dengan pagination
     4. `screens/Admin/Investment/BoxInvestmentListOfInvestor.tsx` - Box
        component untuk list item

### No Issue
2026-02-19 14:06:02 +08:00
f284e2ec02 delete comment 2026-02-18 17:20:28 +08:00
1d61ad51e5 Fixed Admin UI
Admin – Donation Pages
- app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx
- app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx
- app/(application)/admin/donation/[id]/list-of-donatur.tsx
- app/(application)/admin/donation/[status]/status.tsx
- app/(application)/admin/donation/category-update.tsx
- app/(application)/admin/donation/category.tsx

Admin Services
- service/api-admin/api-admin-donation.ts
- service/api-admin/api-master-admin.ts

Admin Screens (Updated)
- screens/Admin/Voting/ScreenEventTypeOfEvent.tsx

Docs
- docs/prompt-for-qwen-code.md

New Donation Components
- screens/Admin/Donation/BoxDonationCategory.tsx
- screens/Admin/Donation/BoxDonationListDisbursementOfFunds.tsx
- screens/Admin/Donation/BoxDonationListOfDonatur.tsx
- screens/Admin/Donation/BoxDonationStatus.tsx

New Donation Screens
- screens/Admin/Donation/ScreenDonationCategory.tsx
- screens/Admin/Donation/ScreenDonationListDisbursementOfFunds.tsx
- screens/Admin/Donation/ScreenDonationListOfDonatur.tsx
- screens/Admin/Donation/ScreenDonationStatus.tsx

### No Issue"
2026-02-18 17:19:46 +08:00
76845b71b4 ## 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

### No Issue"
2026-02-18 14:28:15 +08:00
97e1f50660 Ringkasan Perubahan
Kami telah melakukan serangkaian perubahan pada file app/(application)/admin/event/[id]/[status]/index.tsx
    untuk memperbaiki error dan meningkatkan fungsionalitas aplikasi. Berikut adalah perubahan-perubahan yang
    telah dilakukan:

    1. Perbaikan Fungsi Download QR Code
     - Mengganti implementasi fungsi downloadQRCode yang sebelumnya menggunakan modul native
       (react-native-view-shot dan @react-native-camera-roll/camera-roll) yang menyebabkan error
     - Mengganti dengan implementasi sederhana yang menampilkan pesan bahwa fitur sedang dalam pengembangan
     - Menambahkan pengecekan platform untuk memastikan fitur hanya berjalan di platform yang didukung (non-web)

    2. Pembersihan Kode
     - Menghapus penggunaan useRef karena tidak lagi diperlukan setelah mengganti implementasi
     - Menghapus komponen View yang digunakan sebagai referensi karena tidak lagi diperlukan
     - Menyederhanakan struktur komponen QR code

    3. Perbaikan Tampilan
     - Menyesuaikan tampilan tombol download agar tetap muncul meskipun QR code sedang dimuat
     - Memastikan bahwa tombol download QR tetap terlihat dan fungsional

    4. Penanganan Error
     - Menambahkan penanganan error yang lebih baik untuk mencegah crash aplikasi
     - Mengganti implementasi yang menyebabkan error Invariant Violation terkait modul native yang tidak terdaftar

    Tujuan dari Perubahan Ini

     1. Mengatasi Error Runtime: Mengatasi error Invariant Violation: TurboModuleRegistry.getEnforcing(...):
        'RNViewShot' could not be found dan error terkait modul native lainnya
     2. Meningkatkan Stabilitas Aplikasi: Memastikan bahwa aplikasi tidak mengalami crash akibat modul yang tidak
        terdaftar
     3. Menyederhanakan Fungsionalitas: Menyediakan implementasi sementara untuk fitur download QR code sampai
        konfigurasi native module selesai
     4. Meningkatkan Pengalaman Pengguna: Memastikan bahwa antarmuka tetap responsif dan memberikan umpan balik yang
         jelas kepada pengguna

    File yang Terpengaruh

     - app/(application)/admin/event/[id]/[status]/index.tsx

### No Issue
2026-02-14 15:33:42 +08:00
1cbe4ab330 Ringkasan Perubahan
1. Penambahan Pagination pada Fitur Admin Job
     - Menerapkan sistem pagination menggunakan hook usePagination dari hooks/use-pagination.tsx
     - Mengintegrasikan komponen-komponen pagination dari helpers/paginationHelpers.tsx
     - Menambahkan dukungan infinite scroll dan pull-to-refresh
     - Menambahkan loading state, skeleton loader, dan empty state

    2. Pembaruan Fungsi API
     - Memperbarui fungsi apiAdminJob di service/api-admin/api-admin-job.ts untuk mendukung parameter
       pagination
     - Menambahkan parameter page dengan nilai default 1

    3. Modularisasi Kode
     - Memindahkan komponen AdminJobStatus dari app/(application)/admin/job/[status]/status.tsx ke
       screens/Admin/Job/ScreenJobStatus.tsx
     - Mengganti ViewWrapper dengan NewWrapper untuk tampilan yang lebih fleksibel
     - Membuat komponen baru BoxStatusJob.tsx untuk memisahkan logika tampilan item pekerjaan
     - Menggunakan komponen BoxStatusJob di dalam ScreenJobStatus untuk menampilkan daftar pekerjaan

    4. Perbaikan Struktur dan Organisasi Kode
     - Mengorganisir ulang struktur folder untuk komponen admin job
     - Memisahkan tanggung jawab antara komponen layar dan komponen item
     - Mengoptimalkan performa dengan menggunakan useCallback dan useMemo

    File-file yang Diubah

     1. screens/Admin/Job/ScreenJobStatus.tsx - Implementasi utama dengan pagination
     2. screens/Admin/Job/BoxStatusJob.tsx - Komponen baru untuk menampilkan item pekerjaan
     3. service/api-admin/api-admin-job.ts - Penambahan parameter pagination
     4. app/(application)/admin/job/[status]/status.tsx - Diperbarui untuk menggunakan komponen baru

### NO Issue
2026-02-14 11:57:27 +08:00
42fa80c228 Fix Admin
Admin – App Information
- app/(application)/admin/app-information/index.tsx
- app/(application)/admin/app-information/business-field/[id]/index.tsx

Admin Screens
- screens/Admin/App-Information/BusinessFieldSection.tsx
- screens/Admin/App-Information/InformationBankSection.tsx
- screens/Admin/User-Access/ScreenUserAccess.tsx

New Admin Screens
- screens/Admin/App-Information/ScreenAppInformation.tsx
- screens/Admin/App-Information/ScreenBusinessFieldDetail.tsx

Shared Components
- components/_ShareComponent/Admin/BoxTitlePage.tsx

API Service
- service/api-admin/api-master-admin.ts

Styles
- styles/global-styles.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-13 17:38:48 +08:00
118 changed files with 8300 additions and 5697 deletions

542
QWEN.md
View File

@@ -2,53 +2,68 @@
## 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
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
- **Framework**: Expo (v54.0.0) with React Native (v0.81.5)
- **Language**: TypeScript
- **Architecture**: File-based routing with Expo Router
- **State Management**: Context API
- **State Management**: Context API (AuthContext)
- **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
- **Build System**: Metro bundler
- **Package Manager**: Bun
### Project Structure
```
hipmi-mobile/
├── app/ # Main application screens and routing
├── app/ # Main application screens and routing (Expo Router)
│ ├── _layout.tsx # Root layout component
│ ├── index.tsx # Entry point (Login screen)
│ └── ...
│ └── (application)/ # Main app screens
│ ├── admin/ # Admin panel screens
│ ├── (user)/ # User screens
│ └── ...
├── components/ # Reusable UI components
│ ├── _ShareComponent/ # Shared components (NewWrapper, Admin components)
│ ├── _Icon/ # Icon components
│ └── ...
├── context/ # State management (AuthContext)
├── screens/ # Screen components organized by feature
│ ├── Admin/ # Admin panel screens
│ │ ├── Donation/ # Donation management screens
│ │ ├── Voting/ # Voting management screens
│ │ ├── Event/ # Event management screens
│ │ └── ...
│ ├── Authentication/ # Login, registration flows
│ ├── Collaboration/ # Collaboration features
│ ├── Event/ # Event management
│ ├── Forum/ # Forum functionality
│ ├── Home/ # Home screen
│ ├── Maps/ # Map integration
│ ├── Profile/ # User profile
│ ├── RootLayout/ # Root layout components
│ └── ...
├── 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
│ ├── 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
── 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
### Prerequisites
- Node.js (with bun as the package manager)
- Expo CLI
- iOS Simulator or Android Emulator (for native builds)
- **Node.js**: v18+ with Bun package manager
- **Expo CLI**: Installed globally or via npx
- **iOS**: Xcode (macOS only) for iOS simulator/builds
- **Android**: Android Studio for Android emulator/builds
### Setup and Development
@@ -60,110 +75,441 @@ hipmi-mobile/
2. **Run Development Server**
```bash
bun run start
```
Or use the shorthand:
```bash
# or
bunx expo start
```
3. **Platform-Specific Commands**
- iOS: `bun run ios` or `bunx expo start --ios`
- Android: `bun run android` or `bunx expo start --android`
- Web: `bun run web` or `bunx expo start --web`
```bash
# iOS Simulator
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**
```bash
bun run lint
```
### Environment Variables
The application uses environment variables defined in the app.config.js file:
- `API_BASE_URL`: Base URL for API endpoints
- `BASE_URL`: Base application URL
- `DEEP_LINK_URL`: URL for deep linking functionality
### Build Commands
### EAS Build Configuration
The project uses Expo Application Services (EAS) for building and deploying:
- Development builds with development client
- Preview builds for internal distribution
- Production builds for app stores
#### EAS Build (Production)
```bash
# Production build (App Store / Play Store)
eas build --profile production
## Features and Functionality
# Preview build (Internal distribution)
eas build --profile preview
The application appears to include several key modules:
- **Authentication**: Login, registration, and verification flows
- **Admin Panel**: Administrative functions
- **Collaboration**: Tools for member collaboration
- **Events**: Event management and calendar
- **Forum**: Discussion forums
- **Maps**: Location-based services with Mapbox integration
- **Donations**: Donation functionality
- **Job Board**: Employment opportunities
- **Investment**: Investment-related features
- **Voting**: Voting systems
- **Portfolio**: Member portfolio showcase
- **Notifications**: Push notifications via Firebase
# Development build (Development client)
eas build --profile development
```
#### Local Native Builds
```bash
# Generate native folders (iOS & Android)
npx expo prebuild
# iOS specific
bunx expo prebuild --platform ios
open ios/HIPMIBadungConnect.xcworkspace
# 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
### Coding Standards
- TypeScript is used throughout the project for type safety
- Component-based architecture with reusable components
- Context API for state management
- File-based routing with Expo Router
- Consistent naming conventions using camelCase for variables and PascalCase for components
- **TypeScript**: Strict mode enabled
- **Naming**:
- Components: PascalCase (`Admin_ScreenDonationStatus`)
- Files: PascalCase for components (`ScreenDonationStatus.tsx`)
- 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
- Linting is configured with ESLint
- Standard Expo linting configuration is used
- Linting: `bun run lint`
- No formal test suite configured yet
### Security
- Firebase is integrated for authentication and messaging
- Camera and location permissions are properly configured
- Deep linking is secured with app domain associations
### Git Workflow
- Feature branches: `feature/xxx` or `fixed-admin/xxx`
- Commit messages: Clear and descriptive
- Use CHANGE_LOG.md for tracking changes
## Key Dependencies
## Key Features
### Core Dependencies
- `@react-navigation/*`: Navigation solution for React Native
- `@react-native-firebase/*`: Firebase integration for React Native
- `@rnmapbox/maps`: Mapbox integration for React Native
- `expo-router`: File-based routing for Expo applications
- `react-native-paper`: Material Design components for React Native
- `react-native-toast-message`: Toast notifications
- `react-native-otp-entry`: OTP input components
- `react-native-qrcode-svg`: QR code generation
### Authentication
- Phone number login with OTP
- User registration
- Terms & Conditions acceptance
- Session persistence with AsyncStorage
### Development Dependencies
- `@types/*`: TypeScript type definitions
- `eslint-config-expo`: Expo-specific ESLint configuration
- `typescript`: Type checking
### Admin Module
- **Dashboard**: Overview and statistics
- **User Access**: User management
- **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:
- **iOS**: With tablet support and proper permissions
- **Android**: With adaptive icons and intent filters for deep linking
- **Web**: Static output configuration for web deployment
## API Configuration
## 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
- Bundle identifier: `com.anonymous.hipmi-mobile`
- Supports tablets
- Google Services integration
- Location permission handling
- Associated domains for deep linking
### Axios Interceptor
All API calls use `apiConfig` with automatic token injection:
```typescript
apiConfig.interceptors.request.use(async (config) => {
const token = await AsyncStorage.getItem("authToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
### Android Configuration
- Package name: `com.bip.hipmimobileapp`
- Adaptive icons
- Edge-to-edge display enabled
- Intent filters for HTTPS deep linking
- Google Services integration
## Platform Configuration
### Maps Integration
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
### iOS
- **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
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
### Android
- **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-staging-hipmi.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)

View File

@@ -100,8 +100,8 @@ packagingOptions {
applicationId 'com.bip.hipmimobileapp'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 4
versionName "1.0.1"
versionCode 1
versionName "1.0.2"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}

View File

@@ -4,7 +4,7 @@ require("dotenv").config();
export default {
name: "HIPMI Badung Connect",
slug: "hipmi-mobile",
version: "1.0.1",
version: "1.0.2",
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "hipmimobile",
@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "20",
buildNumber: "3",
},
android: {
@@ -32,7 +32,7 @@ export default {
},
edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp",
versionCode: 4,
versionCode: 1,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{
@@ -77,7 +77,6 @@ export default {
},
],
"expo-font",
"@rnmapbox/maps",
"@react-native-firebase/app",
[
"expo-notifications",
@@ -87,6 +86,7 @@ export default {
iosDisplayInForeground: true,
},
],
"@maplibre/maplibre-react-native",
],
experiments: {

View File

@@ -105,7 +105,7 @@ export default function TakePicture() {
</Pressable>
<Pressable onPress={pickImage}>
<AntDesign name="folderopen" size={32} color="white" />
<AntDesign name="folder-open" size={32} color="white" />
</Pressable>
</View>
</View>

View File

@@ -1,66 +1,93 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell";
import { stylesHome } from "@/screens/Home/homeViewStyle";
import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import { RefreshControl, View } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore();
const [listData, setListData] = useState<any[] | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataJob();
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();
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
try {
const response = await apiUser(user?.id as string);
setData(response.data);
} catch (error: any) {
console.log("[ERROR onLoadData]", error?.message);
console.log("[ERROR Response]", error?.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 response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
try {
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(() => {
setRefreshing(true);
onLoadData();
onLoadDataJob();
checkVersion();
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) {
console.log("User is not active");
console.warn("User is not active");
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
@@ -69,7 +96,7 @@ export default function Application() {
}
if (data && data?.Profile === null) {
console.log("Profile is null");
console.warn("Profile is null");
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
@@ -77,31 +104,39 @@ export default function Application() {
);
}
if (data && data?.masterUserRoleId !== "1") {
console.log("User is not admin");
return (
<BasicWrapper>
<Redirect href={`/admin/dashboard`} />
</BasicWrapper>
);
}
// if (data && data?.masterUserRoleId !== "1") {
// console.log("User is not admin");
// return (
// <BasicWrapper>
// <Redirect href={`/admin/dashboard`} />
// </BasicWrapper>
// );
// }
return (
<>
<Stack.Screen
options={{
title: `HIPMI`,
headerLeft: () => (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
),
headerRight: () => <HeaderBell />,
headerLeft: () =>
data ? (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
headerRight: () =>
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
}}
/>
<ViewWrapper
@@ -114,24 +149,51 @@ export default function Application() {
/>
}
footerComponent={
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
data && data ? (
<TabSection
tabs={tabsHome({
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>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<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>
</ViewWrapper>
</>

View File

@@ -1,244 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
export default function MapsEdit() {
const { id } = useLocalSearchParams();
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>
);
return <Maps_ScreenMapsEdit />;
}
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
marginBottom: 20,
},
map: {
flex: 1,
},
});

View File

@@ -1,143 +1,9 @@
import {
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";
import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
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 (
<ViewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<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>
<>
<Maps_ScreenMapsCreate />
</>
);
}

View File

@@ -1,6 +1,4 @@
import MapsView from "@/screens/Maps/MapsView";
import MapsView2 from "@/screens/Maps/MapsView2";
import { Text, View } from "react-native";
export interface LocationItem {
id: string | number;
@@ -13,8 +11,14 @@ export interface LocationItem {
export default function Maps() {
return (
<>
<MapsView />
{/* <MapsView2 />, */}
{/* <Stack.Screen
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 File

@@ -10,6 +10,7 @@ import {
} from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -49,7 +50,7 @@ export default function Portofolio() {
useCallback(() => {
onLoadData(id as string);
onLoadUserByToken();
}, [id])
}, [id]),
);
async function onLoadData(id: string) {
@@ -64,6 +65,8 @@ export default function Portofolio() {
setProfileId(response?.data?.Profile?.id);
};
return (
<>
{/* Header */}
@@ -87,18 +90,24 @@ export default function Portofolio() {
/>
<ViewWrapper>
{!data || !profileId ? (
<LoaderCustom />
<StackCustom>
<CustomSkeleton height={400} />
<CustomSkeleton height={300} />
</StackCustom>
) : (
<StackCustom>
<Portofolio_Data
data={data}
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
/>
<Portofolio_BusinessLocation
data={data?.BusinessMaps}
imageId={data?.logoId}
setOpenDrawerLocation={setOpenDrawerLocation}
/>
{data?.BusinessMaps && (
<Portofolio_BusinessLocation
data={data?.BusinessMaps}
imageId={data?.logoId}
setOpenDrawerLocation={setOpenDrawerLocation}
/>
)}
<Portofolio_SocialMediaSection
data={data?.Portofolio_MediaSosial}
/>
@@ -135,36 +144,38 @@ export default function Portofolio() {
closeDrawer={() => setOpenDrawerLocation(false)}
height={"auto"}
>
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
{data?.BusinessMaps?.imageId && (
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
rightItem={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={
rightItem={
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
}
/>
@@ -172,26 +183,26 @@ export default function Portofolio() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
rightItem={<TextCustom>{data?.tlpn}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
rightItem={<TextCustom>{data?.alamatKantor}</TextCustom>}
/>
<Spacing />

View File

@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { NewWrapper, StackCustom } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom";
import { MainColor } from "@/constants/color-palet";
@@ -16,8 +16,8 @@ import { GStyles } from "@/styles/global-styles";
import { IProfile } from "@/types/Type-Profile";
import { Ionicons } from "@expo/vector-icons";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import React, { useCallback, useState } from "react";
import { TouchableOpacity } from "react-native";
import { useCallback, useState } from "react";
import { RefreshControl, TouchableOpacity } from "react-native";
export default function Profile() {
const { id } = useLocalSearchParams();
@@ -25,6 +25,7 @@ export default function Profile() {
const [data, setData] = useState<IProfile>();
const [dataToken, setDataToken] = useState<IProfile>();
const [listPortofolio, setListPortofolio] = useState<any[]>();
const [refreshing, setRefreshing] = useState(false);
const { token, logout, isAdmin, user, userData } = useAuth();
@@ -43,7 +44,7 @@ export default function Profile() {
onLoadUserByToken();
isUserCheck();
userData(token as string);
}, [id, token])
}, [id, token]),
);
const isUserCheck = () => {
@@ -54,13 +55,21 @@ export default function Profile() {
};
const onLoadData = async (id: string) => {
const response = await apiProfile({ id: id });
setData(response.data);
try {
const response = await apiProfile({ id: id });
setData(response.data);
} catch (error) {
console.log("[ERROR onLoadData]", error);
}
};
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
try {
const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
} catch (error) {
console.log("[ERROR onLoadUserByToken]", error);
}
};
const onLoadPortofolio = async (id: string) => {
@@ -69,15 +78,25 @@ export default function Profile() {
const lastTwoByDate = response.data
.sort(
(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
.slice(0, 2);
setListPortofolio(lastTwoByDate);
} 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 (
<>
<Stack.Screen
@@ -97,9 +116,21 @@ export default function Profile() {
}}
/>
{/* Main View */}
<ViewWrapper>
<NewWrapper
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
>
{!data || !dataToken ? (
<LoaderCustom />
<StackCustom>
<CustomSkeleton height={400} />
<CustomSkeleton height={200} />
</StackCustom>
) : (
<>
<ProfileSection data={data as any} />
@@ -110,7 +141,7 @@ export default function Profile() {
/>
</>
)}
</ViewWrapper>
</NewWrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom

View File

@@ -1,135 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenBusinessFieldDetail } from "@/screens/Admin/App-Information/ScreenBusinessFieldDetail";
export default function AdminAppInformation_BusinessFieldDetail() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenBusinessFieldDetail />;
}

View File

@@ -1,86 +1,5 @@
import { ScrollableCustom, StackCustom, ViewWrapper } from "@/components";
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";
import { Admin_ScreenAppInformation } from "@/screens/Admin/App-Information/ScreenAppInformation";
export default function AdminInformation() {
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
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>
</>
);
return <Admin_ScreenAppInformation />;
}
const listPage = [
{
id: "1",
label: "Informasi Bank",
value: "bank",
},
{
id: "2",
label: "Bidang & Sub Bidang",
value: "business",
},
{
id: "3",
label: "Stiker",
value: "sticker",
},
];

View File

@@ -1,12 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
ButtonCustom,
Grid,
StackCustom,
TextCustom,
ViewWrapper,
BaseBox,
BoxButtonOnFooter,
ButtonCustom,
Grid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
@@ -22,7 +22,7 @@ export default function AdminCollaborationPublish() {
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [status])
}, [status]),
);
const handlerLoadData = async () => {
@@ -78,16 +78,16 @@ export default function AdminCollaborationPublish() {
</StackCustom>
</BaseBox>
{data?.report && (
<BaseBox>
<GridTwoView
spanLeft={4}
spanRight={8}
leftIcon={<TextCustom bold>Catatan report</TextCustom>}
rightIcon={<TextCustom>{data?.report}</TextCustom>}
/>
</BaseBox>
)}
{data?.report && (
<BaseBox>
<GridTwoView
spanLeft={4}
spanRight={8}
leftItem={<TextCustom bold>Catatan report</TextCustom>}
rightItem={<TextCustom>{data?.report}</TextCustom>}
/>
</BaseBox>
)}
</ViewWrapper>
</>
);

View File

@@ -42,19 +42,19 @@ export default function AdminDonationDetailDisbursementOfFunds() {
const listData = [
{
label: "Nominal",
value: `Rp ${(data && formatCurrencyDisplay(data?.nominalCair)) || 0}`,
value: `Rp ${data ? formatCurrencyDisplay(data?.nominalCair) : 0}`,
},
{
label: "Tanggal",
value: dateTimeView({ date: data?.createdAt }),
value: data ? dateTimeView({ date: data?.createdAt }) : "-",
},
{
label: "Judul",
value: (data && data?.title) || "-",
value: data ? data?.title : "-",
},
{
label: "Deskripsi",
value: (data && data?.deskripsi) || "-",
value: data ? data?.deskripsi : "-",
},
];
return (

View File

@@ -1,126 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenDonationListDisbursementOfFunds } from "@/screens/Admin/Donation/ScreenDonationListDisbursementOfFunds";
export default function AdminDonasiListOfDisbursementOfFunds() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenDonationListDisbursementOfFunds />;
}

View File

@@ -1,186 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenDonationListOfDonatur } from "@/screens/Admin/Donation/ScreenDonationListOfDonatur";
export default function AdminDonasiListOfDonatur() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenDonationListOfDonatur />;
}

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenDonationStatus } from "@/screens/Admin/Donation/ScreenDonationStatus";
export default function AdminDonationStatus() {
const { status } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenDonationStatus />;
}

View File

@@ -30,7 +30,6 @@ export default function AdminDonationCategoryUpdate() {
const response = await apiAdminMasterDonationCategoryById({
id: id as any,
});
console.log(JSON.stringify(response.data, null, 2));
setData(response.data);
};
@@ -44,10 +43,9 @@ export default function AdminDonationCategoryUpdate() {
id: id as any,
data: data,
});
console.log(JSON.stringify(response.data, null, 2));
router.back();
} catch (error) {
console.log(error);
console.log("Error update category:", error);
} finally {
setIsLoading(false);
}

View File

@@ -1,135 +1,5 @@
import {
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";
import { Admin_ScreenDonationCategory } from "@/screens/Admin/Donation/ScreenDonationCategory";
export default function AdminDonationCategory() {
const [listData, setListData] = useState<any[]>([]);
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>
</>
);
return <Admin_ScreenDonationCategory />;
}

View File

@@ -1,254 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenEventDetail } from "@/screens/Admin/Event/ScreenEventDetail";
export default function AdminEventDetail() {
const { user } = useAuth();
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>
</>
);
return <Admin_ScreenEventDetail />;
}

View File

@@ -1,105 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
export default function AdminEventListOfParticipants() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenEventListOfParticipants />;
}

View File

@@ -1,135 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
export default function AdminEventStatus() {
const { status } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenEventStatus />;
}

View File

@@ -1,129 +1,5 @@
import {
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";
import { Admin_ScreenEventTypeOfEvent } from "@/screens/Admin/Voting/ScreenEventTypeOfEvent";
export default function AdminEventTypeOfEvent() {
const [listData, setListData] = useState<any[] | null>(null);
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>
</>
);
return <Admin_ScreenEventTypeOfEvent />;
}

View File

@@ -26,7 +26,7 @@ export default function AdminForumDetailPosting() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -72,6 +72,10 @@ export default function AdminForumDetailPosting() {
label: "Total Report",
value: data?.JumlahReportPosting || 0,
},
{
label: "Postingan",
value: (data && data?.diskusi) || "-",
},
];
return (
@@ -111,13 +115,6 @@ export default function AdminForumDetailPosting() {
))}
</StackCustom>
</BaseBox>
<BaseBox>
<StackCustom gap={"sm"}>
<TextCustom bold>Postingan</TextCustom>
<TextCustom>{(data && data?.diskusi) || "-"}</TextCustom>
</StackCustom>
</BaseBox>
</ViewWrapper>
<DrawerCustom

View File

@@ -1,91 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumListComment } from "@/screens/Admin/Forum/ScreenForumListComment";
export default function AdminForumListComment() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenForumListComment />;
}

View File

@@ -1,262 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumDetailReportComment } from "@/screens/Admin/Forum/ScreenForumDetailReportComment";
export default function AdminForumReportComment() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenForumDetailReportComment />;
}

View File

@@ -1,283 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumDetailReportPosting } from "@/screens/Admin/Forum/ScreenForumDetailReportPosting";
export default function AdminForumReportPosting() {
const { user } = useAuth();
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>
</>
);
export default function AdminForumDetailReportPosting() {
return <Admin_ScreenForumDetailReportPosting />;
}

View File

@@ -1,121 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumPosting } from "@/screens/Admin/Forum/ScreenForumPosting";
export default function AdminForumPosting() {
const [list, setList] = useState<any | null>(null);
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>
</>
);
return <Admin_ScreenForumPosting />;
}

View File

@@ -1,137 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumReportComment } from "@/screens/Admin/Forum/ScreenForumReportComment";
export default function AdminForumReportComment() {
const [listData, setListData] = useState<any[] | null>(null);
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>
</>
);
return <Admin_ScreenForumReportComment />;
}

View File

@@ -1,124 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenForumReportPosting } from "@/screens/Admin/Forum/ScreenForumReportPosting";
export default function AdminForumReportPosting() {
const [listData, setListData] = useState<any[] | null>(null);
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>
</>
);
return <Admin_ScreenForumReportPosting />;
}

View File

@@ -20,6 +20,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
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 GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
@@ -182,9 +183,9 @@ export default function AdminInvestmentDetail() {
<BaseBox>
<StackCustom>
<GridSpan_4_8
label={<TextCustom bold>File Prospektus</TextCustom>}
value={
<GridTwoView
leftItem={<TextCustom bold>File Prospektus</TextCustom>}
rightItem={
<ButtonCustom
iconLeft={
<IconProspectus
@@ -202,9 +203,10 @@ export default function AdminInvestmentDetail() {
</ButtonCustom>
}
/>
<GridSpan_4_8
label={<TextCustom bold>File Dokumen</TextCustom>}
value={
<GridTwoView
leftItem={<TextCustom bold>File Dokumen</TextCustom>}
rightItem={
<StackCustom>
{_.isEmpty(data?.DokumenInvestasi) ? (
<TextCustom align="center">-</TextCustom>

View File

@@ -1,13 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BadgeCustom,
BaseBox,
ButtonCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
ButtonCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
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 { useAuth } from "@/hooks/use-auth";
import {
apiAdminInvestmentGetOneInvoiceById,
apiAdminInvestmentUpdateInvoice,
apiAdminInvestmentGetOneInvoiceById,
apiAdminInvestmentUpdateInvoice,
} from "@/service/api-admin/api-admin-investment";
import { colorBadgeTransaction } from "@/utils/colorBadge";
import { dateTimeView } from "@/utils/dateTimeView";
@@ -60,7 +60,7 @@ export default function AdminInvestmentTransactionDetail() {
value: (data && data?.MasterBank?.namaBank) || "-",
},
{
label: "Jumlah Investasi",
label: "Nominal",
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
},
{
@@ -158,7 +158,7 @@ export default function AdminInvestmentTransactionDetail() {
spanRight={6}
styleLeft={{ paddingRight: 10 }}
styleRight={{ paddingLeft: 10 }}
leftIcon={
leftItem={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
@@ -181,7 +181,7 @@ export default function AdminInvestmentTransactionDetail() {
Tolak
</ButtonCustom>
}
rightIcon={
rightItem={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}

View File

@@ -1,195 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenInvestmentListOfInvestor } from "@/screens/Admin/Investment/ScreenInvestmentListOfInvestor";
export default function AdminInvestmentListOfInvestor() {
const { id } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenInvestmentListOfInvestor />;
}

View File

@@ -1,113 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenInvestmentStatus } from "@/screens/Admin/Investment/ScreenInvestmentStatus";
export default function AdminInvestmentStatus() {
const { status } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenInvestmentStatus />;
}

View File

@@ -5,6 +5,7 @@ import {
BaseBox,
DummyLandscapeImage,
Grid,
NewWrapper,
Spacing,
StackCustom,
TextCustom,
@@ -120,7 +121,7 @@ export default function AdminJobDetailStatus() {
return (
<>
<ViewWrapper
<NewWrapper
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
>
<BaseBox>
@@ -184,7 +185,7 @@ export default function AdminJobDetailStatus() {
/>
)}
<Spacing />
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -2,6 +2,7 @@
import {
AlertDefaultSystem,
BoxButtonOnFooter,
NewWrapper,
TextAreaCustom,
ViewWrapper,
} from "@/components";
@@ -100,7 +101,7 @@ export default function AdminJobRejectInput() {
return (
<>
<ViewWrapper
<NewWrapper
footerComponent={buttonSubmit}
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
>
@@ -112,7 +113,7 @@ export default function AdminJobRejectInput() {
showCount
maxLength={1000}
/>
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
export default function AdminJobStatus() {
const { status } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenJobStatus />;
}

View File

@@ -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 API_IMAGE from "@/constants/api-storage";
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router";
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() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -45,7 +36,7 @@ export default function AdminMaps() {
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [])
}, []),
);
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 (
<>
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
{/* <MapCustom height={"100%"} /> */}
<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>
<MapsV2Custom markers={markers} />
</ViewWrapper>
<DrawerCustom
@@ -138,58 +85,60 @@ export default function AdminMaps() {
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
{selected.imageId && (
<DummyLandscapeImage height={200} imageId={selected.imageId} />
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
rightItem={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>

View File

@@ -1,17 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import {
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
} from "@/service/api-admin/api-admin-user-access";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
@@ -26,7 +26,7 @@ export default function SuperAdminDetail() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -48,7 +48,7 @@ export default function SuperAdminDetail() {
const response = await apiAdminUserAccessUpdateStatus({
id: id as string,
role: data?.masterUserRoleId === "2" ? "user" : "admin",
category: "role"
category: "role",
});
if (!response.success) {
@@ -102,8 +102,8 @@ export default function SuperAdminDetail() {
key={index}
spanLeft={4}
spanRight={8}
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
rightIcon={<TextCustom>{item?.value}</TextCustom>}
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
rightItem={<TextCustom>{item?.value}</TextCustom>}
/>
))}
</StackCustom>

View File

@@ -1,18 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
} from "@/service/api-admin/api-admin-user-access";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
@@ -28,7 +28,7 @@ export default function AdminUserAccessDetail() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -102,8 +102,8 @@ export default function AdminUserAccessDetail() {
key={index}
spanLeft={4}
spanRight={8}
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
rightIcon={<TextCustom>{item?.value}</TextCustom>}
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
rightItem={<TextCustom>{item?.value}</TextCustom>}
/>
))}
</StackCustom>

View File

@@ -5,6 +5,7 @@ import {
BaseBox,
CircleContainer,
Grid,
NewWrapper,
Spacing,
StackCustom,
TextCustom,
@@ -13,7 +14,7 @@ import {
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 GridTwoView from "@/components/_ShareComponent/GridTwoView";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
@@ -40,7 +41,7 @@ export default function AdminVotingDetail() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -169,26 +170,28 @@ export default function AdminVotingDetail() {
return (
<>
<ViewWrapper
<NewWrapper
hideFooter
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
>
<BaseBox>
<StackCustom>
{listData.map((item, i) => (
<GridSpan_4_8
<GridTwoView
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
spanLeft={5}
spanRight={7}
leftItem={<TextCustom bold>{item.label}</TextCustom>}
rightItem={<TextCustom>{item.value}</TextCustom>}
/>
))}
</StackCustom>
</BaseBox>
{status === "publish" ||
(status === "history" && (
<BaseBox>
<TextCustom bold align="center">
Hasil Voting
{(status === "publish" || status === "history") && (
<BaseBox>
<TextCustom bold align="center">
Hasil Voting
</TextCustom>
<Spacing />
<Grid>
@@ -209,11 +212,11 @@ export default function AdminVotingDetail() {
</TextCustom>
</StackCustom>
</Grid.Col>
)
),
)}
</Grid>
</BaseBox>
))}
)}
{data &&
data?.catatan &&
@@ -250,7 +253,7 @@ export default function AdminVotingDetail() {
/>
)}
<Spacing />
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenVotingStatus } from "@/screens/Admin/Voting/ScreenVotingStatus";
export default function AdminVotingStatus() {
const { status } = useLocalSearchParams();
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>
</>
);
return <Admin_ScreenVotingStatus />;
}

View File

@@ -1,104 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
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";
import { Admin_ScreenVotingHistory } from "@/screens/Admin/Voting/ScreenVotingHistory";
export default function AdminVotingHistory() {
const [list, setList] = useState<any | null>(null);
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>
</>
);
return <Admin_ScreenVotingHistory />;
}

1556
bun.lock

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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,
},
});

View 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,
},
});

View File

@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
});
console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) {
console.error("❌ Gagal mendaftarkan device token:", error);
} catch (error: any) {
// 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
}
};

View File

@@ -24,8 +24,8 @@ export default function AdminComp_BoxTitle({
style={{
backgroundColor: AccentColor.darkblue,
borderColor: AccentColor.blue,
padding: 10,
paddingBlock: 5,
paddingInline: 10,
borderWidth: 1,
borderRadius: 10,
}}

View File

@@ -4,15 +4,15 @@ import Grid from "../Grid/GridCustom";
export default function GridTwoView({
spanLeft = 6,
spanRight = 6,
leftIcon,
rightIcon,
leftItem,
rightItem,
styleLeft,
styleRight,
}: {
spanLeft?: number;
spanRight?: number;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
leftItem?: React.ReactNode;
rightItem?: React.ReactNode;
styleLeft?: ViewStyle;
styleRight?: ViewStyle;
}) {
@@ -24,13 +24,13 @@ export default function GridTwoView({
span={spanLeft}
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
>
{leftIcon}
{leftItem}
</Grid.Col>
<Grid.Col
span={spanRight}
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
>
{rightIcon}
{rightItem}
</Grid.Col>
</Grid>
);

View File

@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
right: 0,
height: 100,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 4,
borderRadius: 1,
}}
/>
</View>

28
docs/CHANGE_LOG.md Normal file
View 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
View 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

View File

@@ -22,8 +22,8 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
File refrensi: screens/Donation/ScreenListOfNews.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
File refrensi: screens/Admin/Event/ScreenEventStatus.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
<!-- ===================== End Penerapan Pagination ` ===================== -->
@@ -53,31 +53,71 @@ Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- End Penerapan NewWrapper -->
<!-- Start Random Prompt -->
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar
<!-- End Random Prompt -->
<!-- START Prompt Admin Refactoring -->
File source: app/(application)/admin/user-access/index.tsx
Folder tujuan: screens/Admin/User-Access
Nama file utama: ScreenUserAccess.tsx
Nama function utama: Admin_ScreenUserAccess
<!-- Pindah kode ke Screen Component -->
File source: app/(application)/admin/event/[id]/[status]/index.tsx
Folder tujuan: screens/Admin/Event
Nama file utama: ScreenEventDetail.tsx
Nama function utama: Admin_ScreenEventDetail
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Function fecth: apiAdminUserAccessGetAll
File function fetch: service/api-admin/api-admin-user-access.ts
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
<!-- Penerapan Pagination -->
Function fecth: apiAdminForumCommentById
File function fetch: service/api-admin/api-admin-forum.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
Kemudian rapikan code nya pisah komponen seperti render item dan lainnya agar lebih rapi dan di dalam return panggil komponen tersebut
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- END Prompt Admin Refactoring -->
<!-- END Prompt Admin Refactoring -->
<!-- Additional -->
File refrensi: screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationListOfDonatur.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfInvestor.tsx
<!-- Use Prompt Now -->
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
<!-- Create FlatList -->
File Utama: screens/Admin/App-Information/InformationBankSection.tsx
Terapkan FlatList dan pagination pada file "File Utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
<!-- Create Box -->
File Utama: app/(application)/(user)/maps/[id]/edit.tsx
Folder tujuan: screens/Maps
Nama file utama: ScreenMapsEdit.tsx
Nama function utama: Maps_ScreenMapsEdit
Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
<!-- END Create Box -->
<!-- END Use Prompt Now -->

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
{
"originHash" : "e70d3525c8e2819a8b34f22909815dab5c700c25a06c32388f3930f7b3627768",
"pins" : [
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
"state" : {
"revision" : "c68c970ff3ece56cfc3b36849db70167fa208beb",
"version" : "6.17.1"
}
}
],
"version" : 3
}

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>20</string>
<string>3</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -35,13 +35,6 @@ target 'HIPMIBadungConnect' do
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']
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
pre_install do |installer|
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
$RNMapboxMaps.pre_install(installer)
# @generated end @rnmapbox/maps-pre_installer
end
# @generated end pre_installer
use_react_native!(
:path => config[:reactNativePath],
@@ -56,9 +49,9 @@ target 'HIPMIBadungConnect' do
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
post_install do |installer|
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
$RNMapboxMaps.post_install(installer)
# @generated end @rnmapbox/maps-post_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|

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@maplibre/maplibre-react-native": "^10.4.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-firebase/app": "^23.7.0",
@@ -21,7 +22,6 @@
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@rnmapbox/maps": "^10.2.7",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",
@@ -33,7 +33,7 @@
"expo-dev-client": "~6.0.12",
"expo-device": "^8.0.9",
"expo-document-picker": "~14.0.7",
"expo-file-system": "^19.0.15",
"expo-file-system": "^19.0.21",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
@@ -52,7 +52,7 @@
"moti": "^0.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native": "0.81.5",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",

View File

@@ -1,114 +1,56 @@
import {
ActionIcon,
BadgeCustom,
CenterCustom,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
TextCustom
} from "@/components";
import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminMasterBusinessField } from "@/service/api-admin/api-master-admin";
import { FontAwesome5 } from "@expo/vector-icons";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { router } from "expo-router";
export default function AdminAppInformation_BusinessFieldSection() {
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
);
const onLoadList = async () => {
try {
setLoadData(true);
const response = await apiAdminMasterBusinessField();
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR LIST BUSINESS FIELD]", error);
setListData([]);
} finally {
setLoadData(false);
}
interface Bidang {
item: {
id: string;
name: string;
slug: string;
active: boolean;
createdAt: string;
updatedAt: string;
};
}
export default function AdminAppInformation_BusinessFieldSection({
item,
}: {
item: any;
}) {
return (
<>
<StackCustom>
<AdminBasicBox
onPress={() =>
router.push(`/admin/app-information/business-field/${item.item.id}`)
}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<Grid>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<TextCustom bold>Aksi</TextCustom>
<Grid.Col span={8} style={{ alignSelf: "center" }}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
{item?.item?.name || "-"}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "center" }}>
<TextCustom bold>Status</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold>Nama Bidang Bisnis</TextCustom>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<CenterCustom>
{item?.item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</CenterCustom>
</Grid.Col>
</Grid>
<Divider />
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
<StackCustom>
{listData?.map((item: any, index: number) => (
<View key={index}>
<Grid>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<ActionIcon
icon={
<FontAwesome5
name="edit"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(
`/admin/app-information/business-field/${item.id}`
);
}}
/>
</Grid.Col>
<Grid.Col
span={4}
style={{ alignItems: "center", justifyContent: "center" }}
>
<CenterCustom>
<BadgeCustom
color={
item.active ? AccentColor.blue : AccentColor.blackgray
}
>
{item.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom>{item.name}</TextCustom>
</Grid.Col>
</Grid>
</View>
))}
</StackCustom>
)}
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -1,119 +1,59 @@
import {
ActionIcon,
BadgeCustom,
CenterCustom,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
TextCustom
} from "@/components";
import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminMasterBank } from "@/service/api-admin/api-master-admin";
import { FontAwesome5 } from "@expo/vector-icons";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { router } from "expo-router";
export default function AdminAppInformation_Bank() {
const [listData, setListData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
loadMasterBank();
}, [])
);
const loadMasterBank = async () => {
try {
setLoadData(true);
const response = await apiAdminMasterBank();
setListData(response.data);
} catch (error) {
console.log("[ERROR LIST BANK]", error);
setListData([]);
} finally {
setLoadData(false);
}
interface BankProps {
item: {
id: string;
namaBank: string;
namaAkun: string;
norek: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
}
export default function AdminAppInformation_Bank({
item,
}: {
item: BankProps;
}) {
return (
<>
<StackCustom>
<AdminBasicBox
onPress={() =>
router.push(`/admin/app-information/information-bank/${item.item.id}`)
}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<Grid>
<Grid.Col span={3}>
<TextCustom bold align="center">
Aksi
</TextCustom>
<Grid.Col span={8}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
{item?.item?.namaBank || "-"}
</TextCustom>
<TextCustom size={"small"} bold truncate color="gray">
{item?.item?.norek || "-"}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={3}>
<TextCustom bold align="center">
Status
</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold align="center">
Nama Bank
</TextCustom>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<CenterCustom>
{item?.item?.isActive ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</CenterCustom>
</Grid.Col>
</Grid>
<Divider />
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
<StackCustom>
{listData?.map((item: any, index: number) => (
<View key={index}>
<Grid>
<Grid.Col span={3} style={{ alignItems: "center" }}>
<ActionIcon
icon={
<FontAwesome5
name="edit"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(
`/admin/app-information/information-bank/${item.id}`
);
}}
/>
</Grid.Col>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<CenterCustom>
<BadgeCustom
color={
item.isActive
? AccentColor.blue
: AccentColor.blackgray
}
>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom align="center">{item.namaBank}</TextCustom>
</Grid.Col>
</Grid>
</View>
))}
</StackCustom>
)}
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,153 @@
import { ScrollableCustom, StackCustom } from "@/components";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
import AdminAppInformation_Bank_Component from "@/screens/Admin/App-Information/InformationBankSection";
import { apiFetchAdminMasterAppInformation } from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { Alert, RefreshControl } from "react-native";
export function Admin_ScreenAppInformation() {
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
const [activePage, setActivePage] = useState<string>("Informasi Bank");
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiFetchAdminMasterAppInformation({
category: activeCategory as string,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [activeCategory],
onError: (error) => console.error("[ERROR] Fetch job by status:", error),
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
const handlePress = (item: any) => {
setActiveCategory(item.value);
setActivePage(item.label);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory]),
);
const scrollComponent = (
<StackCustom>
<ScrollableCustom
data={listPage.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
<AdminComp_BoxTitle
title={activePage}
rightComponent={
<AdminActionIconPlus
onPress={() => {
if (activeCategory === "bank") {
router.push("/admin/app-information/information-bank/create");
} else if (activeCategory === "business") {
router.push("/admin/app-information/business-field/create");
} else if (activeCategory === "sticker") {
Alert.alert("Coming Soon", "Next Update");
// router.push("/admin/app-information/sticker/create");
}
}}
/>
}
/>
</StackCustom>
);
// const renderContent = () => {
// switch (activeCategory) {
// case "bank":
// return <AdminAppInformation_Bank_Component />;
// case "business":
// return <AdminAppInformation_BusinessFieldSection />;
// // case "sticker":
// // return <AdminAppInformation_StickerSection />;
// default:
// return <AdminAppInformation_Bank_Component />;
// }
// };
const renderItem = (item: any) => {
if (activeCategory === "bank") {
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
} else if (activeCategory === "business") {
return (
<AdminAppInformation_BusinessFieldSection key={item.id} item={item} />
);
} else {
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
}
};
return (
<NewWrapper
headerComponent={scrollComponent}
// ListHeaderComponent={
// }
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
// Data dan render
listData={pagination.listData}
renderItem={(item: any) => renderItem(item)}
/>
// {renderContent()}
// </NewWrapper>
);
}
const listPage = [
{
id: "1",
label: "Informasi Bank",
value: "bank",
},
{
id: "2",
label: "Bidang & Sub Bidang",
value: "business",
},
// {
// id: "3",
// label: "Stiker",
// value: "sticker",
// },
];

View File

@@ -0,0 +1,185 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
CenterCustom,
NewWrapper,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import {
ICON_SIZE_SMALL,
PAGINATION_DEFAULT_TAKE,
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
import { Ionicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
export function Admin_ScreenBusinessFieldDetail() {
const { id } = useLocalSearchParams();
const [bidang, setBidang] = useState<any | null>(null);
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiAdminMasterBusinessFieldById({
category: "only-sub-bidang",
id: id as any,
page: String(page),
});
// Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) => {
console.log("Error fetching data sub bidang", error);
},
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: "",
emptyMessage: "Tidak ada data pengguna",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
isInitialLoad: pagination.isInitialLoad,
});
useFocusEffect(
useCallback(() => {
onLoadBidang();
pagination.onRefresh();
}, [id]),
);
const onLoadBidang = async () => {
try {
const response = await apiAdminMasterBusinessFieldById({
id: id as string,
category: "all",
});
setBidang(response.data);
} catch (error) {
console.log("[ERROR]", error);
setBidang(null);
}
};
const renderHeader = () => (
<View>
<BaseBox
onPress={() =>
router.push(
`/admin/app-information/business-field/${id}/bidang-update`,
)
}
>
<StackCustom gap={"xs"}>
<GridSpan_NewComponent
span1={10}
span2={2}
text1={
<StackCustom>
<TextCustom bold size={"large"}>
{bidang?.bidang?.name}
</TextCustom>
{bidang?.bidang.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</StackCustom>
}
text2={
<CenterCustom>
<Ionicons
name="caret-forward"
size={ICON_SIZE_SMALL}
color="white"
/>
</CenterCustom>
}
/>
</StackCustom>
</BaseBox>
<CenterCustom>
<TextCustom bold>Sub Bidang</TextCustom>
</CenterCustom>
<Spacing height={5} />
</View>
);
const renderItem = ({ item }: { item: any }) => (
<BaseBox
onPress={() =>
router.push(
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`,
)
}
>
<StackCustom gap={"xs"}>
<GridSpan_NewComponent
span1={10}
span2={2}
text1={
<StackCustom>
<TextCustom bold size={"large"}>
{item.name}
</TextCustom>
{item?.isActive ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</StackCustom>
}
text2={
<CenterCustom>
<Ionicons
name="caret-forward"
size={ICON_SIZE_SMALL}
color="white"
/>
</CenterCustom>
}
/>
</StackCustom>
</BaseBox>
);
return (
<>
<NewWrapper
listData={pagination.listData}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
headerComponent={
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
}
ListHeaderComponent={renderHeader()}
renderItem={renderItem}
/>
</>
);
}

View File

@@ -0,0 +1,36 @@
import { TextCustom, BadgeCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { router } from "expo-router";
import { View } from "react-native";
export default function Admin_BoxDonationCategory({item}: {item: any}) {
return (
<>
<AdminBasicBox
onPress={() => {
router.push(`/admin/donation/category-update?id=${item.id}`);
}}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<GridTwoView
leftItem={<TextCustom bold>{item?.name || "-"}</TextCustom>}
rightItem={
<View>
{item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</View>
}
spanLeft={8}
spanRight={4}
styleRight={{
alignItems: "flex-end",
}}
/>
</AdminBasicBox>
</>
)
}

View File

@@ -0,0 +1,53 @@
import { Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import dayjs from "dayjs";
import { router } from "expo-router";
import { View } from "react-native";
interface BoxDonationListDisbursementOfFundsProps {
item: any;
}
export default function Admin_BoxDonationListDisbursementOfFunds({
item,
}: BoxDonationListDisbursementOfFundsProps) {
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/detail-disbursement-of-funds`,
);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Tanggal</TextCustom>}
value={
<TextCustom>
{dayjs(item?.createdAt).format("DD-MM-YYYY") || "-"}
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Nominal</TextCustom>}
value={
<TextCustom bold>
Rp {formatCurrencyDisplay(item?.nominalCair)}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,68 @@
import {
BadgeCustom,
Divider,
StackCustom,
TextCustom
} from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { colorBadgeTransaction } from "@/utils/colorBadge";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router } from "expo-router";
import _ from "lodash";
import { View } from "react-native";
interface BoxDonationListOfDonaturProps {
item: any;
}
export default function Admin_BoxDonationListOfDonatur({
item,
}: BoxDonationListOfDonaturProps) {
const statusName = item?.DonasiMaster_StatusInvoice?.name || "-";
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/${_.lowerCase(
item?.DonasiMaster_StatusInvoice?.name,
)}/transaction-detail`,
);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate>
{item?.Author?.username || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Status</TextCustom>}
value={
<BadgeCustom
color={colorBadgeTransaction({
status: statusName,
})}
>
{statusName}
</BadgeCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Nominal</TextCustom>}
value={
<TextCustom>
{item?.nominal
? `Rp ${formatCurrencyDisplay(item?.nominal)}`
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,54 @@
import { Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router } from "expo-router";
import { View } from "react-native";
interface BoxDonationStatusProps {
item: any;
status?: string;
}
export default function Admin_BoxDonationStatus({
item,
status,
}: BoxDonationStatusProps) {
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/donation/${item.id}/${status}`);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Durasi</TextCustom>}
value={
<TextCustom>
{item?.DonasiMaster_Durasi?.name || "-"} hari
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Target Dana</TextCustom>}
value={
<TextCustom>
{item?.target
? `Rp ${formatCurrencyDisplay(item?.target)}`
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,107 @@
import { BadgeCustom, TextCustom } from "@/components";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl, View } from "react-native";
import Admin_BoxDonationCategory from "./BoxDonationCategory";
export function Admin_ScreenDonationCategory() {
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminMasterDonationCategory({
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen action plus untuk header
const rightComponent = useMemo(
() => (
<AdminActionIconPlus
onPress={() => {
router.push(`/admin/donation/category-create`);
}}
/>
),
[],
);
// Header component untuk title
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title="Kategori Donasi"
rightComponent={rightComponent}
/>
),
[rightComponent],
);
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Render item untuk daftar kategori donasi
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxDonationCategory key={index} item={item} />
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,82 @@
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
import { useLocalSearchParams } from "expo-router";
import { useCallback, useMemo } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxDonationListDisbursementOfFunds from "./BoxDonationListDisbursementOfFunds";
export function Admin_ScreenDonationListDisbursementOfFunds() {
const { id } = useLocalSearchParams();
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminDonationDisbursementOfFundsListById({
id: id as string,
category: "get-all",
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: "",
dependencies: [id],
});
// Header component dengan back button dan title
const headerComponent = useMemo(
() => <AdminBackButtonAntTitle title="Daftar Pencairan Dana" />,
[],
);
// Render item untuk daftar pencairan dana
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxDonationListDisbursementOfFunds key={index} item={item} />
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: "",
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,134 @@
import { SelectCustom } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
import { apiMasterTransaction } from "@/service/api-client/api-master";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useMemo, useState } from "react";
import { RefreshControl, View } from "react-native";
import Admin_BoxDonationListOfDonatur from "./BoxDonationListOfDonatur";
export function Admin_ScreenDonationListOfDonatur() {
const { id } = useLocalSearchParams();
const [selectValue, setSelectValue] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
const [master, setMaster] = useState<any[]>([]);
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminDonationListOfDonatur({
id: id as string,
status: selectedStatus as any,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: "",
dependencies: [id, selectedStatus],
});
// Load master data untuk select option
useEffect(() => {
onLoadMaster();
}, []);
const onLoadMaster = async () => {
try {
const response = await apiMasterTransaction();
if (response.success) {
setMaster(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setMaster([]);
}
};
// Komponen select untuk filter status
const searchComponent = useMemo(
() => (
<View style={{ flexDirection: "row", gap: 5 }}>
<SelectCustom
placeholder="Pilih status transaksi"
data={
_.isEmpty(master)
? []
: master?.map((item: any) => ({
label: item.name,
value: item.id,
}))
}
value={selectValue}
onChange={(value: any) => {
setSelectValue(value);
const nameSelected = master.find((item: any) => item.id === value);
const statusChooses = _.lowerCase(nameSelected?.name);
setSelectedStatus(statusChooses);
}}
styleContainer={{ width: "100%", marginBottom: 0 }}
allowClear
/>
</View>
),
[master, selectValue],
);
// Header component dengan back button dan select filter
const headerComponent = useMemo(
() => <AdminBackButtonAntTitle newComponent={searchComponent} />,
[searchComponent],
);
// Render item untuk daftar donatur
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxDonationListOfDonatur key={index} item={item} />
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: "",
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,112 @@
import { ActionIcon, SearchInput, TextCustom } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
ICON_SIZE_BUTTON,
PAGINATION_DEFAULT_TAKE,
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
import { Octicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxDonationStatus from "./BoxDonationStatus";
export function Admin_ScreenDonationStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminDonation({
category: status as "publish" | "review" | "reject",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari judul donasi"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
// Render item untuk daftar donasi
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxDonationStatus
key={index}
item={item}
status={status as string}
/>
),
[status],
);
// Header component dengan judul status donasi
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Donasi ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 120,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,85 @@
import { BadgeCustom, BaseBox, Spacing, StackCustom, TextCustom } from "@/components";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { colorBadgeStatus } from "@/utils/colorBadge";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
interface EventDetailData {
Author?: {
username?: string;
};
title?: string;
lokasi?: string;
EventMaster_TipeAcara?: {
name?: string;
};
tanggal?: string;
tanggalSelesai?: string;
deskripsi?: string;
catatan?: string;
}
interface BoxEventDetailProps {
data: EventDetailData | null;
status: string;
}
export function BoxEventDetail({ data, status }: BoxEventDetailProps) {
const listData = [
{
label: "Pembuat Event",
value: data?.Author?.username || "-",
},
{
label: "Judul Event",
value: data?.title || "-",
},
{
label: "Status",
value: data ? (
<BadgeCustom color={colorBadgeStatus({ status })}>
{_.startCase(status)}
</BadgeCustom>
) : (
"-"
),
},
{
label: "Lokasi",
value: data?.lokasi || "-",
},
{
label: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
label: "Mulai Event",
value: data?.tanggal ? dateTimeView({ date: data.tanggal }) : "-",
},
{
label: "Event Berakhir",
value: data?.tanggalSelesai
? dateTimeView({ date: data.tanggalSelesai })
: "-",
},
{
label: "Deskripsi",
value: data?.deskripsi || "-",
},
];
return (
<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>
);
}

View File

@@ -0,0 +1,55 @@
import {
BadgeCustom,
BaseBox,
Grid,
StackCustom,
TextCustom
} from "@/components";
import dayjs from "dayjs";
import { View } from "moti";
interface Admin_BoxEventParticipantProps {
item: any;
startDate?: dayjs.Dayjs;
}
export function Admin_BoxEventParticipant({
item,
startDate,
}: Admin_BoxEventParticipantProps) {
return (
<BaseBox>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"sm"}>
<TextCustom bold truncate>
{item?.User?.username}
</TextCustom>
<TextCustom>+{item?.User?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
{startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color={item?.isPresent ? "green" : "red"}
>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom style={{ alignSelf: "flex-end" }} color="gray">
-
</BadgeCustom>
</View>
)}
</Grid.Col>
</Grid>
</BaseBox>
);
}

View File

@@ -0,0 +1,48 @@
import { StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { dateTimeView } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { View } from "react-native";
import { Divider } from "react-native-paper";
interface Admin_BoxEventStatusProps {
item: any;
status: string;
}
export function Admin_BoxEventStatus({ item, status }: Admin_BoxEventStatusProps) {
return (
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/event/${item.id}/${status}`);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Mulai</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.tanggal }) || "-"}
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Berakhir</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.tanggalSelesai }) || "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
);
}

View File

@@ -0,0 +1,37 @@
import { DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
import { IconList } from "@/components/_Icon/IconComponent";
import { router } from "expo-router";
interface EventDetailDrawerProps {
isVisible: boolean;
onClose: () => void;
eventId: string;
}
export function EventDetailDrawer({
isVisible,
onClose,
eventId,
}: EventDetailDrawerProps) {
return (
<DrawerCustom
isVisible={isVisible}
closeDrawer={onClose}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Daftar Peserta",
icon: <IconList />,
path: `/admin/event/${eventId}/list-of-participants`,
},
]}
onPressItem={(item) => {
onClose();
router.push(item.path as any);
}}
/>
</DrawerCustom>
);
}

View File

@@ -0,0 +1,26 @@
import { BaseBox, LoaderCustom, Spacing, StackCustom, TextCustom } from "@/components";
import QRCode from "react-native-qrcode-svg";
interface EventDetailQRCodeProps {
qrValue: string;
isLoading: boolean;
}
export function EventDetailQRCode({ qrValue, isLoading }: EventDetailQRCodeProps) {
return (
<BaseBox>
<StackCustom style={{ alignItems: "center" }}>
<TextCustom bold>QR Code Event</TextCustom>
{isLoading ? (
<LoaderCustom />
) : (
<QRCode
value={qrValue}
size={200}
/>
)}
</StackCustom>
<Spacing />
</BaseBox>
);
}

View File

@@ -0,0 +1,163 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ActionIcon, AlertDefaultSystem } from "@/components";
import { IconDot } 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 ReportBox from "@/components/Box/ReportBox";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
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 { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import Toast from "react-native-toast-message";
import { BoxEventDetail } from "./BoxEventDetail";
import { EventDetailDrawer } from "./EventDetailDrawer";
import { EventDetailQRCode } from "./EventDetailQRCode";
export function Admin_ScreenEventDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = 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 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);
}
};
const headerComponent = useMemo(
() => (
<AdminBackButtonAntTitle
title={`Detail Data`}
rightComponent={
status === "publish" || status === "history"
? rightComponent
: undefined
}
/>
),
[status],
);
const footerComponent = useMemo(() => {
if (status === "review") {
return (
<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}`);
}}
/>
);
}
if (status === "reject") {
return (
<AdminButtonReject
title="Tambah Catatan"
onReject={() => {
router.push(`/admin/event/${id}/reject-input?status=${status}`);
}}
/>
);
}
return null;
}, [status, id]);
return (
<>
<NewWrapper
headerComponent={headerComponent}
footerComponent={footerComponent}
>
<BoxEventDetail data={data} status={status as string} />
{data?.catatan && (status === "reject" || status === "review") && (
<ReportBox text={data.catatan} />
)}
{(status === "publish" || status === "history") && (
<EventDetailQRCode qrValue={isDevLink} isLoading={loadData} />
)}
</NewWrapper>
<EventDetailDrawer
isVisible={openDrawer}
onClose={() => setOpenDrawer(false)}
eventId={id as string}
/>
</>
);
}

View File

@@ -0,0 +1,83 @@
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
import dayjs from "dayjs";
import { useLocalSearchParams } from "expo-router";
import { useCallback } from "react";
import { RefreshControl } from "react-native";
import { Admin_BoxEventParticipant } from "./BoxEventParticipant";
export function Admin_ScreenEventListOfParticipants() {
const { id } = useLocalSearchParams();
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminEventListOfParticipants({
id: id as string,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) => {
console.error("Error loading participants:", error);
},
});
// Render item untuk daftar peserta
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxEventParticipant
key={index}
item={item}
startDate={dayjs(item?.Event?.tanggal)}
/>
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: "",
emptyMessage: "Belum ada peserta",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 60,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id.toString()}
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
);
}

View File

@@ -0,0 +1,108 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import { Admin_BoxEventStatus } from "./BoxEventStatus";
export function Admin_ScreenEventStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminEvent({
category: status as
| "publish"
| "review"
| "history"
| "dashboard"
| "type-of-event",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen kanan untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
// Render item untuk daftar event
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxEventStatus key={index} item={item} status={status as string} />
),
[status],
);
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Event ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id.toString()}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
);
}

View File

@@ -0,0 +1,292 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
DrawerCustom,
MenuDrawerDynamicGrid,
StackCustom,
TextCustom,
} from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import {
apiAdminForumCommentById,
apiAdminForumDeactivateComment,
apiAdminForumListReportCommentById,
} from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Toast from "react-native-toast-message";
export function Admin_ScreenForumDetailReportComment() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPage, setOpenDrawerPage] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false);
const [data, setData] = useState<any | null>(null);
const [selectedReport, setSelectedReport] = useState({
id: "",
username: "",
kategori: "",
keterangan: "",
deskripsi: "",
});
// Load data komentar saat screen fokus
useFocusEffect(
useCallback(() => {
onLoadDataKomentar();
}, [id]),
);
// Pagination untuk list report comment
const pagination = usePagination({
fetchFunction: async (page) => {
const response = await apiAdminForumListReportCommentById({
id: id as string,
page: String(page),
});
if (response.success) {
return { data: response.data };
}
return { data: [] };
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
});
const onLoadDataKomentar = async () => {
try {
const response = await apiAdminForumCommentById({
id: id as string,
category: "get-one",
});
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Render item untuk daftar report comment
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item?.id,
username: item?.User?.username,
kategori: item?.ForumMaster_KategoriReport?.title,
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item?.deskripsi,
});
}}
>
<StackCustom gap={0}>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Pelapor</TextCustom>}
rightItem={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
/>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Jenis Laporan</TextCustom>}
rightItem={
<TextCustom truncate={2}>
{item
? item?.ForumMaster_KategoriReport?.title
? item?.ForumMaster_KategoriReport?.title
: "Lainnya"
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Header component dengan back button dan menu
const headerComponent = useMemo(
() => (
<AdminBackButtonAntTitle
title="Report Komentar"
rightComponent={
<ActionIcon
icon={<IconDot size={16} color={MainColor.darkblue} />}
onPress={() => setOpenDrawerPage(true)}
/>
}
/>
),
[],
);
// Detail komentar component
const ListHeader = useMemo(
() => (
<AdminBasicBox>
<StackCustom gap={"sm"}>
<GridSpan_4_8
label={<TextCustom bold>Username</TextCustom>}
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridSpan_4_8
label={<TextCustom bold>Komentar</TextCustom>}
value={<TextCustom>{data?.komentar || "-"}</TextCustom>}
/>
</StackCustom>
</AdminBasicBox>
),
[data],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada report komentar",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<>
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
headerComponent={headerComponent}
ListHeaderComponent={ListHeader}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
{/* Drawer untuk menu halaman (hapus komentar) */}
<DrawerCustom
isVisible={openDrawerPage}
closeDrawer={() => setOpenDrawerPage(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 response = await apiAdminForumDeactivateComment({
id: id as string,
data: {
senderId: user?.id as string,
},
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Komentar gagal dihapus",
});
return;
}
setOpenDrawerPage(false);
Toast.show({
type: "success",
text1: "Komentar berhasil dihapus",
});
router.back();
},
});
}}
/>
</DrawerCustom>
{/* Drawer untuk detail report comment */}
<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>
</>
);
}

View File

@@ -0,0 +1,297 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
DrawerCustom,
MenuDrawerDynamicGrid,
StackCustom,
TextCustom,
} from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import {
apiAdminForumDeactivatePosting,
apiAdminForumListReportPostingById,
apiAdminForumPostingById,
} from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Toast from "react-native-toast-message";
export function Admin_ScreenForumDetailReportPosting() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPage, setOpenDrawerPage] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false);
const [data, setData] = useState<any | null>(null);
const [selectedReport, setSelectedReport] = useState({
id: "",
username: "",
kategori: "",
keterangan: "",
deskripsi: "",
});
// Load data postingan saat screen fokus
useFocusEffect(
useCallback(() => {
onLoadDataPosting();
}, [id]),
);
// Pagination untuk list report
const pagination = usePagination({
fetchFunction: async (page) => {
const response = await apiAdminForumListReportPostingById({
id: id as string,
page: String(page),
});
if (response.success) {
return { data: response.data };
}
return { data: [] };
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
});
const onLoadDataPosting = async () => {
try {
const response = await apiAdminForumPostingById({
id: id as string,
});
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Render item untuk daftar report
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item?.id,
username: item?.User?.username,
kategori: item?.ForumMaster_KategoriReport?.title,
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item?.deskripsi,
});
}}
>
<StackCustom gap={0}>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Pelapor</TextCustom>}
rightItem={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
/>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Jenis Laporan</TextCustom>}
rightItem={
<TextCustom truncate={2}>
{item
? item?.ForumMaster_KategoriReport?.title
? item?.ForumMaster_KategoriReport?.title
: "Lainnya"
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Header component dengan detail postingan
const headerComponent = useMemo(
() => (
<AdminBackButtonAntTitle
title="Detail Report Posting"
rightComponent={
<ActionIcon
icon={<IconDot size={16} color={MainColor.darkblue} />}
onPress={() => setOpenDrawerPage(true)}
/>
}
/>
),
[],
);
// Detail postingan component
const ListHeader = useMemo(
() => (
<AdminBasicBox>
<StackCustom gap={0}>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom bold>Username</TextCustom>}
rightItem={
<TextCustom>{data ? data?.Author?.username : "-"}</TextCustom>
}
/>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom bold>Postingan</TextCustom>}
rightItem={<TextCustom>{data ? data?.diskusi : "-"}</TextCustom>}
/>
</StackCustom>
</AdminBasicBox>
),
[data],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada report",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<>
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
headerComponent={headerComponent}
ListHeaderComponent={ListHeader}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
{/* Drawer untuk menu halaman (hapus posting) */}
<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>
</>
);
}

View File

@@ -0,0 +1,105 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminForumCommentById } from "@/service/api-admin/api-admin-forum";
import { router, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import { Divider } from "react-native-paper";
export function Admin_ScreenForumListComment() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerAction, setOpenDrawerAction] = useState(false);
const [selectedComment, setSelectedComment] = useState({
id: "",
komentar: "",
});
// Pagination untuk list comment
const pagination = usePagination({
fetchFunction: async (page) => {
const response = await apiAdminForumCommentById({
id: id as string,
category: "get-all",
page: String(page),
});
if (response.success) {
return { data: response.data };
}
return { data: [] };
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
});
// Render item untuk daftar komentar
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/forum/${item.id}/list-report-comment`);
}}
>
<StackCustom gap={"md"}>
<TextCustom truncate={1}>
Report : {item?.countReport || 0}
</TextCustom>
<Divider />
<TextCustom truncate={2}>{item?.komentar || "-"}</TextCustom>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Header component dengan back button
const headerComponent = useMemo(
() => <AdminBackButtonAntTitle title="Daftar Komentar" />,
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada komentar",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<>
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
</>
);
}

View File

@@ -0,0 +1,133 @@
import {
SearchInput, StackCustom,
TextCustom
} from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl, View } from "react-native";
import { Divider } from "react-native-paper";
export function Admin_ScreenForumPosting() {
const [search, setSearch] = useState("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminForum({
category: "posting",
search: searchQuery || "",
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen search input
const searchComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari postingan"
value={search}
onChangeText={setSearch}
/>
),
[search],
);
// Header component
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={"Forum Posting"}
rightComponent={searchComponent}
/>
),
[searchComponent],
);
// Render item untuk daftar posting
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/forum/${item.id}`);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.diskusi || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Komentar</TextCustom>}
value={
<TextCustom truncate={1}>{item?.komentar || "-"}</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Report</TextCustom>}
value={
<TextCustom truncate={1}>{item?.reportPosting || "-"}</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data posting",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
);
}

View File

@@ -0,0 +1,140 @@
import { SearchInput, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
export function Admin_ScreenForumReportComment() {
const [search, setSearch] = useState("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminForum({
category: "report_comment",
search: searchQuery || "",
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Komponen search input
const searchComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari Komentar"
value={search}
onChangeText={setSearch}
/>
),
[search],
);
// Header component dengan box title
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title="Report Komentar"
rightComponent={searchComponent}
/>
),
[searchComponent],
);
// Render item untuk daftar report comment
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`,
);
}}
>
<StackCustom gap={0}>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Jumlah Report</TextCustom>}
rightItem={
<TextCustom truncate={2}>
{item?.count || "-"}
</TextCustom>
}
/>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Komentar</TextCustom>}
rightItem={
<TextCustom truncate={2}>
{item?.Forum_Komentar?.komentar || "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data report komentar",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,141 @@
import { SearchInput, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
export function Admin_ScreenForumReportPosting() {
const [search, setSearch] = useState("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminForum({
category: "report_posting",
search: searchQuery || "",
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Komponen search input
const searchComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari Postingan"
value={search}
onChangeText={setSearch}
/>
),
[search],
);
// Box title component
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title="Report Posting"
rightComponent={searchComponent}
/>
),
[searchComponent],
);
// Render item untuk daftar report posting
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
style={{ marginHorizontal: 5, marginVertical: 5 }}
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`,
);
}}
>
<StackCustom gap={0}>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Jumlah Report</TextCustom>}
rightItem={
<TextCustom truncate={1}>
{item?.count|| "-"}
</TextCustom>
}
/>
<GridTwoView
spanLeft={5}
spanRight={7}
leftItem={<TextCustom>Postingan</TextCustom>}
rightItem={
<TextCustom truncate={2}>
{item?.Forum_Posting?.diskusi || "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
),
[],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data report posting",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
);
}

View File

@@ -0,0 +1,63 @@
import { BadgeCustom, Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { colorBadgeTransaction } from "@/utils/colorBadge";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router } from "expo-router";
import _ from "lodash";
interface BoxInvestmentListOfInvestorProps {
item: any;
}
export default function Admin_BoxInvestmentListOfInvestor({
item,
}: BoxInvestmentListOfInvestorProps) {
const statusName = item?.StatusInvoice?.name || "-";
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(
`/admin/investment/${item?.id}/${_.lowerCase(
item?.StatusInvoice?.name,
)}/transaction-detail`,
);
}}
>
<StackCustom gap={0}>
<StackCustom style={{ paddingBlock: 8 }}>
<TextCustom size="large" bold truncate>
{item?.Author?.username || "-"}
</TextCustom>
</StackCustom>
<Divider />
<GridSpan_4_8
label={<TextCustom>Status</TextCustom>}
value={
<BadgeCustom
color={colorBadgeTransaction({
status: statusName,
})}
>
{statusName}
</BadgeCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Nominal</TextCustom>}
value={
<TextCustom>
{item?.nominal
? `Rp ${formatCurrencyDisplay(item?.nominal)}`
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,54 @@
import { Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router } from "expo-router";
import { View } from "react-native";
interface BoxInvestmentStatusProps {
item: any;
status?: string;
}
export default function Admin_BoxInvestmentStatus({
item,
status,
}: BoxInvestmentStatusProps) {
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/investment/${item.id}/${status}`);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Durasi</TextCustom>}
value={
<TextCustom>
{item?.MasterPencarianInvestor?.name || "-"} hari
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Target Dana</TextCustom>}
value={
<TextCustom>
{item?.targetDana
? `Rp ${formatCurrencyDisplay(item?.targetDana)}`
: "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,132 @@
import { SelectCustom } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminInvestmentListOfInvestor } from "@/service/api-admin/api-admin-investment";
import { apiMasterTransaction } from "@/service/api-client/api-master";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxInvestmentListOfInvestor from "./BoxInvestmentListOfInvestor";
export function Admin_ScreenInvestmentListOfInvestor() {
const { id } = useLocalSearchParams();
const [selectValue, setSelectValue] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
const [master, setMaster] = useState<any[]>([]);
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminInvestmentListOfInvestor({
id: id as string,
status: selectedStatus as any,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: "",
dependencies: [id, selectedStatus],
});
// Load master data untuk select option
useEffect(() => {
onLoadMaster();
}, []);
const onLoadMaster = async () => {
try {
const response = await apiMasterTransaction();
if (response.success) {
setMaster(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setMaster([]);
}
};
// Komponen select untuk filter status
const searchComponent = useMemo(
() => (
<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
/>
),
[master, selectValue]
);
// Header component dengan back button dan select filter
const headerComponent = useMemo(
() => <AdminBackButtonAntTitle newComponent={searchComponent} />,
[searchComponent]
);
// Render item untuk daftar investor
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxInvestmentListOfInvestor key={index} item={item} />
),
[]
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: "",
emptyMessage: "Belum ada data investor",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,109 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
PAGINATION_DEFAULT_TAKE,
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminInvestment } from "@/service/api-admin/api-admin-investment";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxInvestmentStatus from "./BoxInvestmentStatus";
export function Admin_ScreenInvestmentStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminInvestment({
category: status as "publish" | "review" | "reject",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari judul investasi"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
// Render item untuk daftar investasi menggunakan Box Component
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxInvestmentStatus
key={index}
item={item}
status={status as string}
/>
),
[status],
);
// Header component dengan judul status investasi
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Investasi ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 120,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,29 @@
import { Spacing, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { router } from "expo-router";
import { View } from "react-native";
import { Divider } from "react-native-paper";
interface BoxStatusJobProps {
item: any;
status: string;
}
export function BoxStatusJob({ item, status }: BoxStatusJobProps) {
return (
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(`/admin/job/${item.id}/${status}`);
}}
>
<StackCustom>
<View style={{paddingBlock: 8}}>
<TextCustom size={"large"} align="center" bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
</StackCustom>
</AdminBasicBox>
);
}

View File

@@ -0,0 +1,103 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import { Divider } from "react-native-paper";
import { BoxStatusJob } from "./BoxStatusJob";
export function Admin_ScreenJobStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminJob({
category: status as "publish" | "review" | "reject",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen kanan untuk header
const rightComponent = useMemo(
() => (
<SearchInput
placeholder="Cari perkerjaan"
onChangeText={setSearch}
value={search}
/>
),
[search],
);
// Render item untuk daftar pekerjaan
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<BoxStatusJob key={index} item={item} status={status as string} />
),
[status],
);
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Job ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id.toString()}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
);
}

View File

@@ -5,20 +5,18 @@ import {
Grid,
SearchInput,
StackCustom,
TextCustom
TextCustom,
} from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import {
PAGINATION_DEFAULT_TAKE
} from "@/constants/constans-value";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
import { router } from "expo-router";
import { useState } from "react";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
export function Admin_ScreenUserAccess() {
@@ -54,6 +52,12 @@ export function Admin_ScreenUserAccess() {
isInitialLoad: pagination.isInitialLoad,
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
const rightComponent = () => {
return (
<>

View File

@@ -0,0 +1,54 @@
import { Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { dateTimeView } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { View } from "react-native";
interface BoxVotingStatusProps {
item: any;
status?: string;
path: any;
}
export default function Admin_BoxVotingStatus({
item,
status,
path,
}: BoxVotingStatusProps) {
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(path);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Mulai</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.awalVote }) || "-"}
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Berakhir</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.akhirVote }) || "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,152 @@
import { BadgeCustom, TextCustom } from "@/components";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl, View } from "react-native";
export function Admin_ScreenEventTypeOfEvent() {
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminMasterTypeOfEvent({
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen action plus untuk header
const rightComponent = useMemo(
() => (
<AdminActionIconPlus
onPress={() => {
router.push(`/admin/event/type-create`);
}}
/>
),
[],
);
// Header component untuk title
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle title="Tipe Acara Event" rightComponent={rightComponent} />
),
[rightComponent],
);
// Render header tabel (Aksi, Status, Tipe Acara)
const renderTableHeader = useMemo(
() => (
<>
<GridViewCustomSpan
span1={2}
span2={5}
span3={5}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={
<TextCustom bold align="center">
Status
</TextCustom>
}
component3={<TextCustom bold>Tipe Acara</TextCustom>}
/>
</>
),
[],
);
// Render item untuk daftar tipe event (mengikuti pattern InformationBankSection)
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
onPress={() => {
router.push(`/admin/event/type-update?id=${item.id}`);
}}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<GridTwoView
leftItem={<TextCustom bold>{item?.name || "-"}</TextCustom>}
rightItem={
<View>
{item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</View>
}
spanLeft={8}
spanRight={4}
styleRight={{
alignItems: "flex-end",
}}
/>
</AdminBasicBox>
),
[],
);
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,102 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
PAGINATION_DEFAULT_TAKE
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxVotingStatus from "./BoxVotingStatus";
export function Admin_ScreenVotingHistory() {
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminVoting({
category: "history",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxVotingStatus
key={index}
item={item}
path={`/admin/voting/${item.id}/history`}
/>
),
[],
);
// Header component dengan judul voting history
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle title="Riwayat" rightComponent={rightComponent} />
),
[rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,110 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
PAGINATION_DEFAULT_TAKE
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxVotingStatus from "./BoxVotingStatus";
export function Admin_ScreenVotingStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminVoting({
category: status as any,
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
// Render item untuk daftar voting
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxVotingStatus
key={index}
item={item}
status={status as string}
path={`/admin/voting/${item.id}/${status}`}
/>
),
[status],
);
// Header component dengan judul status voting
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Voting ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -72,16 +72,16 @@ const adminListMenu: NavbarItem[] = [
{ label: "Report Komentar", link: "/admin/forum/report-comment" },
],
},
{
label: "Collaboration",
icon: "people",
links: [
{ label: "Dashboard", link: "/admin/collaboration" },
{ label: "Publish", link: "/admin/collaboration/publish" },
{ label: "Group", link: "/admin/collaboration/group" },
{ label: "Reject", link: "/admin/collaboration/reject" },
],
},
// {
// label: "Collaboration",
// icon: "people",
// links: [
// { label: "Dashboard", link: "/admin/collaboration" },
// { label: "Publish", link: "/admin/collaboration/publish" },
// { label: "Group", link: "/admin/collaboration/group" },
// { label: "Reject", link: "/admin/collaboration/reject" },
// ],
// },
{ label: "Maps", icon: "map", link: "/admin/maps" },
{
label: "App Information",
@@ -165,16 +165,16 @@ const superAdminListMenu: NavbarItem[] = [
{ label: "Report Komentar", link: "/admin/forum/report-comment" },
],
},
{
label: "Collaboration",
icon: "people",
links: [
{ label: "Dashboard", link: "/admin/collaboration" },
{ label: "Publish", link: "/admin/collaboration/publish" },
{ label: "Group", link: "/admin/collaboration/group" },
{ label: "Reject", link: "/admin/collaboration/reject" },
],
},
// {
// label: "Collaboration",
// icon: "people",
// links: [
// { label: "Dashboard", link: "/admin/collaboration" },
// { label: "Publish", link: "/admin/collaboration/publish" },
// { label: "Group", link: "/admin/collaboration/group" },
// { label: "Reject", link: "/admin/collaboration/reject" },
// ],
// },
{ label: "Maps", icon: "map", link: "/admin/maps" },
{
label: "App Information",

View File

@@ -5,7 +5,6 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiCheckCodeOtp } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { registerForPushNotificationsAsync } from "@/utils/notifications";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
@@ -17,8 +16,6 @@ import Toast from "react-native-toast-message";
export default function VerificationView() {
const { nomor } = useLocalSearchParams<{ nomor: string }>();
console.log("[NOMOR]", nomor);
const [inputOtp, setInputOtp] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);

Some files were not shown because too many files have changed in this diff Show More